Skip to content

Commit 2cadb6c

Browse files
authored
Merge pull request #19 from ctriant/token-exchange-fixes
Token exchange related fixes
2 parents c9d86fa + a937c5c commit 2cadb6c

File tree

4 files changed

+54
-25
lines changed

4 files changed

+54
-25
lines changed

doc/server/contents/conf.rst

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -721,7 +721,7 @@ There are two possible ways to configure Token Exchange in OIDC-OP, globally and
721721
For the first case the configuration is passed in the Token Exchange handler throught the
722722
`urn:ietf:params:oauth:grant-type:token-exchange` dictionary in token's `grant_types_supported`.
723723

724-
If present, the token exchange configuration may contain a `policy` dictionary
724+
If present, the token exchange configuration should contain a `policy` dictionary
725725
that defines the behaviour for each subject token type. Each subject token type
726726
is mapped to a dictionary with the keys `callable` (mandatory), which must be a
727727
python callable or a string that represents the path to a python callable, and
@@ -730,7 +730,14 @@ passed to the callable.
730730

731731
The key `""` represents a fallback policy that will be used if the subject token
732732
type can't be found. If a subject token type is defined in the `policy` but is
733-
not in the `subject_token_types_supported` list then it is ignored::
733+
not in the `subject_token_types_supported` list then it is ignored.
734+
735+
The token exchange configuration should also contain a `requested_token_types_supported`
736+
list that defines the supported token types that can be requested through the
737+
`requested_token_type` parameter of a Token Exchange request. In addition, a
738+
default token type that will be returned in the absence of the `requested_token_type`
739+
in the Token Exchange request should be defined through the `default_requested_token_type`
740+
configuration parameter::
734741

735742
"grant_types_supported":{
736743
"urn:ietf:params:oauth:grant-type:token-exchange": {

src/idpyoidc/server/oauth2/token.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -129,12 +129,15 @@ def process_request(self, request: Optional[Union[Message, dict]] = None, **kwar
129129
_context = self.server_get("endpoint_context")
130130

131131
if isinstance(request, TokenExchangeRequest):
132-
requested_token_type = request.get(
133-
"requested_token_type",
134-
self.helper["urn:ietf:params:oauth:grant-type:token-exchange"].config[
132+
if "token_exchange" in _context.cdb[request["client_id"]]:
133+
default_requested_token_type = _context.cdb[request["client_id"]]["token_exchange"][
135134
"default_requested_token_type"
136-
],
137-
)
135+
]
136+
else:
137+
default_requested_token_type = self.helper[
138+
"urn:ietf:params:oauth:grant-type:token-exchange"
139+
].config["default_requested_token_type"]
140+
requested_token_type = request.get("requested_token_type", default_requested_token_type)
138141
_handler_key = TOKEN_TYPES_MAPPING[requested_token_type]
139142
else:
140143
_handler_key = "access_token"

src/idpyoidc/server/oauth2/token_helper.py

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -384,10 +384,6 @@ def __init__(self, endpoint, config=None):
384384
TokenEndpointHelper.__init__(self, endpoint=endpoint, config=config)
385385
if config is None:
386386
self.config = {
387-
"subject_token_types_supported": [
388-
"urn:ietf:params:oauth:token-type:access_token",
389-
"urn:ietf:params:oauth:token-type:refresh_token",
390-
],
391387
"requested_token_types_supported": [
392388
"urn:ietf:params:oauth:token-type:access_token",
393389
"urn:ietf:params:oauth:token-type:refresh_token",
@@ -422,6 +418,8 @@ def post_parse_request(self, request, client_id="", **kwargs):
422418
) as err:
423419
return self.endpoint.error_cls(error="invalid_request", error_description="%s" % err)
424420

421+
self._validate_configuration(config)
422+
425423
_mngr = _context.session_manager
426424
try:
427425
# token exchange is about minting one token based on another
@@ -482,11 +480,6 @@ def _enforce_policy(self, request, token, config):
482480
)
483481

484482
if subject_token_type not in config["policy"]:
485-
if "" not in config["policy"]:
486-
raise ImproperlyConfigured(
487-
"subject_token_type {subject_token_type} missing from "
488-
"policy and no default is defined"
489-
)
490483
subject_token_type = ""
491484

492485
policy = config["policy"][subject_token_type]
@@ -601,6 +594,30 @@ def process_request(self, request, **kwargs):
601594

602595
return self.token_exchange_response(token=new_token)
603596

597+
def _validate_configuration(self, config):
598+
if "requested_token_types_supported" not in config:
599+
raise ImproperlyConfigured(
600+
f"Missing 'requested_token_types_supported'" "from Token Exchange configuration"
601+
)
602+
if "default_requested_token_type" not in config:
603+
raise ImproperlyConfigured(
604+
f"Missing 'default_requested_token_type'" "from Token Exchange configuration"
605+
)
606+
if "policy" not in config:
607+
raise ImproperlyConfigured(f"Missing 'policy' from Token Exchange configuration")
608+
if "" not in config["policy"]:
609+
raise ImproperlyConfigured(
610+
f"Default Token Exchange policy configuration is not defined"
611+
)
612+
if "callable" not in config["policy"][""]:
613+
raise ImproperlyConfigured(
614+
f"Missing 'callable' from default Token Exchange policy configuration"
615+
)
616+
if config["default_requested_token_type"] not in config["requested_token_types_supported"]:
617+
raise ImproperlyConfigured(
618+
f"Unsupported default requested_token_type {config['default_requested_token_type']}"
619+
)
620+
604621

605622
def validate_token_exchange_policy(request, context, subject_token, **kwargs):
606623
if "resource" in request:

tests/test_server_36_oauth2_token_exchange.py

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -244,9 +244,9 @@ def test_token_exchange1(self, token):
244244
Test that token exchange requests work correctly with only the required parameters
245245
present
246246
"""
247-
if list(token.keys())[0] == "refresh_token":
248-
AUTH_REQ["scope"] = ["openid", "offline_access"]
249247
areq = AUTH_REQ.copy()
248+
if list(token.keys())[0] == "refresh_token":
249+
areq["scope"] = ["openid", "offline_access"]
250250

251251
session_id = self._create_session(areq)
252252
grant = self.endpoint_context.authz(session_id, areq)
@@ -288,9 +288,9 @@ def test_token_exchange2(self, token):
288288
"""
289289
Test that token exchange requests work correctly
290290
"""
291-
if list(token.keys())[0] == "refresh_token":
292-
AUTH_REQ["scope"] = ["openid", "offline_access"]
293291
areq = AUTH_REQ.copy()
292+
if list(token.keys())[0] == "refresh_token":
293+
areq["scope"] = ["openid", "offline_access"]
294294

295295
session_id = self._create_session(areq)
296296
grant = self.endpoint_context.authz(session_id, areq)
@@ -342,6 +342,7 @@ def test_token_exchange_per_client(self, token):
342342
"urn:ietf:params:oauth:token-type:access_token",
343343
"urn:ietf:params:oauth:token-type:refresh_token",
344344
],
345+
"default_requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
345346
"policy": {
346347
"": {
347348
"callable": "idpyoidc.server.oauth2.token_helper.validate_token_exchange_policy",
@@ -350,9 +351,10 @@ def test_token_exchange_per_client(self, token):
350351
},
351352
}
352353

353-
if list(token.keys())[0] == "refresh_token":
354-
AUTH_REQ["scope"] = ["openid", "offline_access"]
355354
areq = AUTH_REQ.copy()
355+
if list(token.keys())[0] == "refresh_token":
356+
areq["scope"] = ["openid", "offline_access"]
357+
356358

357359
session_id = self._create_session(areq)
358360
grant = self.endpoint_context.authz(session_id, areq)
@@ -509,8 +511,8 @@ def test_refresh_token_audience(self):
509511
"""
510512
Test that requesting a refresh token with audience fails.
511513
"""
512-
AUTH_REQ["scope"] = ["openid", "offline_access"]
513514
areq = AUTH_REQ.copy()
515+
areq["scope"] = ["openid", "offline_access"]
514516

515517
session_id = self._create_session(areq)
516518
grant = self.endpoint_context.authz(session_id, areq)
@@ -579,8 +581,8 @@ def test_exchange_refresh_token_to_refresh_token(self):
579581
"""
580582
Test whether exchanging a refresh token to another refresh token works.
581583
"""
582-
AUTH_REQ["scope"] = ["openid", "offline_access"]
583584
areq = AUTH_REQ.copy()
585+
areq["scope"] = ["openid", "offline_access"]
584586

585587
session_id = self._create_session(areq)
586588
grant = self.endpoint_context.authz(session_id, areq)
@@ -615,8 +617,8 @@ def test_exchange_refresh_token_to_refresh_token(self):
615617
],
616618
)
617619
def test_exchange_access_token_to_refresh_token(self, scopes):
618-
AUTH_REQ["scope"] = scopes
619620
areq = AUTH_REQ.copy()
621+
areq["scope"] = scopes
620622

621623
session_id = self._create_session(areq)
622624
grant = self.endpoint_context.authz(session_id, areq)

0 commit comments

Comments
 (0)