Skip to content

Commit 7e50501

Browse files
authored
Merge pull request #23 from IdentityPython/token_exchange_client
Token exchange client
2 parents 30e05ed + b0469b6 commit 7e50501

15 files changed

+920
-20
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
77

88
[metadata]
99
name = "idpyoidc"
10-
version = "1.1.1"
10+
version = "1.2.0"
1111
author = "Roland Hedberg"
1212
author_email = "[email protected]"
1313
description = "Everything OAuth2 and OIDC"

src/idpyoidc/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
__author__ = "Roland Hedberg"
2-
__version__ = "1.1.1"
2+
__version__ = "1.2.0"
33

44
import os
55
from typing import Dict

src/idpyoidc/client/client_auth.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -302,11 +302,13 @@ def construct(self, request=None, service=None, http_args=None, **kwargs):
302302

303303
if service.service_name == "refresh_token":
304304
_acc_token = find_token(request, "refresh_token", service, **kwargs)
305+
elif service.service_name == "token_exchange":
306+
_acc_token = find_token(request, "subject_token", service, **kwargs)
305307
else:
306308
_acc_token = find_token(request, "access_token", service, **kwargs)
307309

308310
if not _acc_token:
309-
raise KeyError("No access or refresh token available")
311+
raise KeyError("No bearer token available")
310312

311313
# The authorization value starts with 'Bearer' when bearer tokens
312314
# are used

src/idpyoidc/client/defaults.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
}
1919

2020
DEFAULT_OAUTH2_SERVICES = {
21-
"discovery": {"class": "idpyoidc.client.oauth2.provider_info_discovery.ProviderInfoDiscovery"},
21+
"discovery": {"class": "idpyoidc.client.oauth2.server_metadata.ServerMetadata"},
2222
"authorization": {"class": "idpyoidc.client.oauth2.authorization.Authorization"},
2323
"access_token": {"class": "idpyoidc.client.oauth2.access_token.AccessToken"},
2424
"refresh_access_token": {

src/idpyoidc/client/oauth2/provider_info_discovery.py renamed to src/idpyoidc/client/oauth2/server_metadata.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,14 @@
1212
LOGGER = logging.getLogger(__name__)
1313

1414

15-
class ProviderInfoDiscovery(Service):
16-
"""The service that talks to the OAuth2 provider info discovery endpoint."""
15+
class ServerMetadata(Service):
16+
"""The service that talks to the OAuth2 server metadata endpoint."""
1717

1818
msg_type = oauth2.Message
1919
response_cls = oauth2.ASConfigurationResponse
2020
error_msg = ResponseMessage
2121
synchronous = True
22-
service_name = "provider_info"
22+
service_name = "server_metadata"
2323
http_method = "GET"
2424

2525
metadata_attributes = {}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
"""Implements the service that can exchange one token for another."""
2+
import logging
3+
4+
from idpyoidc.client.oauth2.utils import get_state_parameter
5+
from idpyoidc.client.service import Service
6+
from idpyoidc.exception import MissingParameter
7+
from idpyoidc.exception import MissingRequiredAttribute
8+
from idpyoidc.message import oauth2
9+
from idpyoidc.message.oauth2 import ResponseMessage
10+
from idpyoidc.time_util import time_sans_frac
11+
12+
LOGGER = logging.getLogger(__name__)
13+
14+
15+
class TokenExchange(Service):
16+
"""The token exchange service."""
17+
18+
msg_type = oauth2.TokenExchangeRequest
19+
response_cls = oauth2.TokenExchangeResponse
20+
error_msg = ResponseMessage
21+
endpoint_name = "token_endpoint"
22+
synchronous = True
23+
service_name = "token_exchange"
24+
default_authn_method = "client_secret_basic"
25+
http_method = "POST"
26+
request_body_type = "urlencoded"
27+
response_body_type = "json"
28+
29+
30+
def __init__(self, client_get, conf=None):
31+
Service.__init__(self, client_get, conf=conf)
32+
self.pre_construct.append(self.oauth_pre_construct)
33+
34+
def update_service_context(self, resp, key="", **kwargs):
35+
if "expires_in" in resp:
36+
resp["__expires_at"] = time_sans_frac() + int(resp["expires_in"])
37+
self.client_get("service_context").state.store_item(resp, "token_response", key)
38+
39+
def oauth_pre_construct(self, request_args=None, post_args=None, **kwargs):
40+
"""
41+
42+
:param request_args: Initial set of request arguments
43+
:param kwargs: Extra keyword arguments
44+
:return: Request arguments
45+
"""
46+
if request_args is None:
47+
request_args = {}
48+
49+
if 'subject_token' not in request_args:
50+
try:
51+
_key = get_state_parameter(request_args, kwargs)
52+
except MissingParameter:
53+
raise MissingRequiredAttribute("subject_token")
54+
55+
parameters = {'access_token', 'scope'}
56+
57+
_state = self.client_get("service_context").state
58+
59+
_args = _state.extend_request_args(
60+
{}, oauth2.AuthorizationResponse, "auth_response", _key, parameters
61+
)
62+
_args = _state.extend_request_args(
63+
_args, oauth2.AccessTokenResponse, "token_response", _key, parameters
64+
)
65+
_args = _state.extend_request_args(
66+
_args, oauth2.AccessTokenResponse, "refresh_token_response", _key, parameters
67+
)
68+
69+
request_args["subject_token"] = _args["access_token"]
70+
request_args["subject_token_type"] = 'urn:ietf:params:oauth:token-type:access_token'
71+
if 'scope' not in request_args and "scope" in _args:
72+
request_args["scope"] = _args["scope"]
73+
74+
return request_args, post_args

src/idpyoidc/client/oidc/provider_info_discovery.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import logging
22

33
from idpyoidc.client.exception import ConfigurationError
4-
from idpyoidc.client.oauth2 import provider_info_discovery
4+
from idpyoidc.client.oauth2 import server_metadata
55
from idpyoidc.message import oidc
66
from idpyoidc.message.oauth2 import ResponseMessage
77

@@ -61,14 +61,16 @@ def add_redirect_uris(request_args, service=None, **kwargs):
6161
return request_args, {}
6262

6363

64-
class ProviderInfoDiscovery(provider_info_discovery.ProviderInfoDiscovery):
64+
class ProviderInfoDiscovery(server_metadata.ServerMetadata):
6565
msg_type = oidc.Message
6666
response_cls = oidc.ProviderConfigurationResponse
6767
error_msg = ResponseMessage
68+
service_name = "provider_info"
69+
6870
metadata_attributes = {}
6971

7072
def __init__(self, client_get, conf=None):
71-
provider_info_discovery.ProviderInfoDiscovery.__init__(self, client_get, conf=conf)
73+
server_metadata.ServerMetadata.__init__(self, client_get, conf=conf)
7274

7375
def update_service_context(self, resp, **kwargs):
7476
_context = self.client_get("service_context")

src/idpyoidc/client/service.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,13 @@
1616
from idpyoidc.message.oauth2 import ResponseMessage
1717
from idpyoidc.message.oauth2 import is_error_message
1818
from idpyoidc.util import importer
19-
20-
from ..constant import JOSE_ENCODED
21-
from ..constant import JSON_ENCODED
22-
from ..constant import URL_ENCODED
2319
from .configure import Configuration
2420
from .exception import ResponseError
2521
from .util import get_http_body
2622
from .util import get_http_url
23+
from ..constant import JOSE_ENCODED
24+
from ..constant import JSON_ENCODED
25+
from ..constant import URL_ENCODED
2726

2827
__author__ = "Roland Hedberg"
2928

@@ -452,6 +451,7 @@ def get_request_parameters(
452451
content_type = JSON_ENCODED
453452

454453
_info["body"] = get_http_body(request, content_type)
454+
455455
_headers.update({"Content-Type": content_type})
456456

457457
if _headers:
@@ -655,7 +655,7 @@ def construct_uris(self, base_url, hex):
655655
uri = self.usage_to_uri_map.get(usage)
656656
if uri and uri not in self.metadata:
657657
self.metadata[uri] = self.get_uri(base_url, self.callback_path[uri],
658-
hex)
658+
hex)
659659

660660
def get_metadata(self, attribute, default=None):
661661
try:
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import logging
2+
3+
from idpyoidc.message import oauth2
4+
5+
from idpyoidc.message import oidc
6+
from idpyoidc.server.endpoint import Endpoint
7+
8+
logger = logging.getLogger(__name__)
9+
10+
11+
class ServerMetadata(Endpoint):
12+
request_cls = oauth2.Message
13+
response_cls = oauth2.ASConfigurationResponse
14+
request_format = ""
15+
response_format = "json"
16+
name = "server_metadata"
17+
18+
def __init__(self, server_get, **kwargs):
19+
Endpoint.__init__(self, server_get=server_get, **kwargs)
20+
self.pre_construct.append(self.add_endpoints)
21+
22+
def add_endpoints(self, request, client_id, endpoint_context, **kwargs):
23+
for endpoint in [
24+
"authorization_endpoint",
25+
"registration_endpoint",
26+
"token_endpoint",
27+
"userinfo_endpoint",
28+
"end_session_endpoint",
29+
]:
30+
endp_instance = self.server_get("endpoint", endpoint)
31+
if endp_instance:
32+
request[endpoint] = endp_instance.endpoint_path
33+
34+
return request
35+
36+
def process_request(self, request=None, **kwargs):
37+
return {"response_args": self.server_get("endpoint_context").provider_info}

src/idpyoidc/server/oauth2/token.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from idpyoidc.server.exception import ProcessError
1414
from idpyoidc.server.oauth2.token_helper import AccessTokenHelper
1515
from idpyoidc.server.oauth2.token_helper import RefreshTokenHelper
16+
from idpyoidc.server.session import MintingNotAllowed
1617
from idpyoidc.server.session.token import TOKEN_TYPES_MAPPING
1718
from idpyoidc.util import importer
1819

@@ -121,6 +122,8 @@ def process_request(self, request: Optional[Union[Message, dict]] = None, **kwar
121122
)
122123
except JWEException as err:
123124
return self.error_cls(error="invalid_request", error_description="%s" % err)
125+
except MintingNotAllowed as err:
126+
return self.error_cls(error="invalid_request", error_description="%s" % err)
124127

125128
if isinstance(response_args, ResponseMessage):
126129
return response_args

0 commit comments

Comments
 (0)