Skip to content

Commit 71f30cd

Browse files
authored
Merge pull request #48 from ctriant/token_exchange_enhancements
Introduce various token exchange enhancements
2 parents cd02bc7 + f9d2ab8 commit 71f30cd

File tree

5 files changed

+815
-31
lines changed

5 files changed

+815
-31
lines changed

src/idpyoidc/server/oauth2/token_helper.py

Lines changed: 46 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ def _mint_token(
7575
token_args = meth(_context, client_id, token_args)
7676

7777
if token_args:
78-
_args = {"token_args": token_args}
78+
_args = token_args
7979
else:
8080
_args = {}
8181

@@ -258,7 +258,6 @@ def process_request(self, req: Union[Message, dict], **kwargs):
258258
if (
259259
issue_refresh
260260
and "refresh_token" in _supports_minting
261-
and "refresh_token" in grant_types_supported
262261
):
263262
try:
264263
refresh_token = self._mint_token(
@@ -370,7 +369,7 @@ def process_request(self, req: Union[Message, dict], **kwargs):
370369
token_type = "DPoP"
371370

372371
token = _grant.get_token(token_value)
373-
scope = _grant.find_scope(token.based_on)
372+
scope = _grant.find_scope(token)
374373
if "scope" in req:
375374
scope = req["scope"]
376375
access_token = self._mint_token(
@@ -543,6 +542,27 @@ def post_parse_request(self, request, client_id="", **kwargs):
543542
)
544543

545544
resp = self._enforce_policy(request, token, config)
545+
if isinstance(resp, TokenErrorResponse):
546+
return resp
547+
548+
scopes = resp.get("scope", [])
549+
scopes = _context.scopes_handler.filter_scopes(scopes, client_id=resp["client_id"])
550+
551+
if not scopes:
552+
logger.error("All requested scopes have been filtered out.")
553+
return self.error_cls(
554+
error="invalid_scope", error_description="Invalid requested scopes"
555+
)
556+
557+
_requested_token_type = resp.get(
558+
"requested_token_type", "urn:ietf:params:oauth:token-type:access_token"
559+
)
560+
_token_class = self.token_types_mapping[_requested_token_type]
561+
if _token_class == "refresh_token" and "offline_access" not in scopes:
562+
return TokenErrorResponse(
563+
error="invalid_request",
564+
error_description="Exchanging this subject token to refresh token forbidden",
565+
)
546566

547567
return resp
548568

@@ -572,7 +592,7 @@ def _enforce_policy(self, request, token, config):
572592
error_description="Unsupported requested token type",
573593
)
574594

575-
request_info = dict(scope=request.get("scope", []))
595+
request_info = dict(scope=request.get("scope", token.scope))
576596
try:
577597
check_unknown_scopes_policy(request_info, request["client_id"], _context)
578598
except UnAuthorizedClientScope:
@@ -602,11 +622,11 @@ def _enforce_policy(self, request, token, config):
602622
logger.error(f"Error while executing the {fn} policy callable: {e}")
603623
return self.error_cls(error="server_error", error_description="Internal server error")
604624

605-
def token_exchange_response(self, token):
625+
def token_exchange_response(self, token, issued_token_type):
606626
response_args = {}
607627
response_args["access_token"] = token.value
608628
response_args["scope"] = token.scope
609-
response_args["issued_token_type"] = token.token_class
629+
response_args["issued_token_type"] = issued_token_type
610630

611631
if token.expires_at:
612632
response_args["expires_in"] = token.expires_at - utc_time_sans_frac()
@@ -636,6 +656,7 @@ def process_request(self, request, **kwargs):
636656
error="invalid_request", error_description="Subject token invalid"
637657
)
638658

659+
grant = _session_info["grant"]
639660
token = _mngr.find_token(_session_info["branch_id"], request["subject_token"])
640661
_requested_token_type = request.get(
641662
"requested_token_type", "urn:ietf:params:oauth:token-type:access_token"
@@ -650,16 +671,19 @@ def process_request(self, request, **kwargs):
650671
if "dpop_signing_alg_values_supported" in _context.provider_info:
651672
if request.get("dpop_jkt"):
652673
_token_type = "DPoP"
674+
scopes = request.get("scope", [])
653675

654676
if request["client_id"] != _session_info["client_id"]:
655677
_token_usage_rules = _context.authz.usage_rules(request["client_id"])
656678

657679
sid = _mngr.create_exchange_session(
658680
exchange_request=request,
681+
original_grant=grant,
659682
original_session_id=sid,
660683
user_id=_session_info["user_id"],
661684
client_id=request["client_id"],
662685
token_usage_rules=_token_usage_rules,
686+
scopes=scopes,
663687
)
664688

665689
try:
@@ -676,25 +700,30 @@ def process_request(self, request, **kwargs):
676700
else:
677701
resources = request.get("audience")
678702

703+
_token_args = None
704+
if resources:
705+
_token_args = {"resources": resources}
706+
679707
try:
680708
new_token = self._mint_token(
681709
token_class=_token_class,
682710
grant=_session_info["grant"],
683711
session_id=sid,
684712
client_id=request["client_id"],
685713
based_on=token,
686-
scope=request.get("scope"),
687-
token_args={"resources": resources},
714+
scope=scopes,
715+
token_args=_token_args,
688716
token_type=_token_type,
689717
)
718+
new_token.expires_at = token.expires_at
690719
except MintingNotAllowed:
691720
logger.error(f"Minting not allowed for {_token_class}")
692721
return self.error_cls(
693722
error="invalid_grant",
694723
error_description="Token Exchange not allowed with that token",
695724
)
696725

697-
return self.token_exchange_response(token=new_token)
726+
return self.token_exchange_response(new_token, _requested_token_type)
698727

699728
def _validate_configuration(self, config):
700729
if "requested_token_types_supported" not in config:
@@ -763,14 +792,13 @@ def validate_token_exchange_policy(request, context, subject_token, **kwargs):
763792
f"forbidden",
764793
)
765794

766-
if "scope" in request:
767-
scopes = list(set(request.get("scope")).intersection(kwargs.get("scope")))
768-
if scopes:
769-
request["scope"] = scopes
770-
else:
771-
return TokenErrorResponse(
772-
error="invalid_request",
773-
error_description="No supported scope requested",
774-
)
795+
scopes = request.get("scope", subject_token.scope)
796+
scopes = list(set(scopes).intersection(subject_token.scope))
797+
if kwargs.get("scope"):
798+
scopes = list(set(scopes).intersection(kwargs.get("scope")))
799+
if scopes:
800+
request["scope"] = scopes
801+
else:
802+
request.pop("scope")
775803

776804
return request

src/idpyoidc/server/session/grant.py

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@ def payload_arguments(
184184
endpoint_context,
185185
item: SessionToken,
186186
claims_release_point: str,
187+
scope: Optional[dict] = None,
187188
extra_payload: Optional[dict] = None,
188189
secondary_identifier: str = "",
189190
) -> dict:
@@ -211,6 +212,10 @@ def payload_arguments(
211212

212213
payload["jti"] = uuid1().hex
213214

215+
if scope is None:
216+
scope = self.scope
217+
payload["scope"] = scope
218+
214219
if extra_payload:
215220
payload.update(extra_payload)
216221

@@ -359,6 +364,7 @@ def mint_token(
359364
endpoint_context,
360365
item=item,
361366
claims_release_point=claims_release_point,
367+
scope=scope,
362368
extra_payload=handler_args,
363369
secondary_identifier=_secondary_identifier,
364370
)
@@ -474,7 +480,7 @@ def get_usage_rules(token_type, endpoint_context, grant, client_id):
474480

475481
class ExchangeGrant(Grant):
476482
parameter = Grant.parameter.copy()
477-
parameter.update({"users": []})
483+
parameter.update({"exchange_request": TokenExchangeRequest, "original_session_id": ""})
478484
type = "exchange_grant"
479485

480486
def __init__(
@@ -483,6 +489,8 @@ def __init__(
483489
claims: Optional[dict] = None,
484490
resources: Optional[list] = None,
485491
authorization_details: Optional[dict] = None,
492+
authorization_request: Optional[Message] = None,
493+
authentication_event: Optional[AuthnEvent] = None,
486494
issued_token: Optional[list] = None,
487495
usage_rules: Optional[dict] = None,
488496
exchange_request: Optional[TokenExchangeRequest] = None,
@@ -501,6 +509,8 @@ def __init__(
501509
claims=claims,
502510
resources=resources,
503511
authorization_details=authorization_details,
512+
authorization_request=authorization_request,
513+
authentication_event=authentication_event,
504514
issued_token=issued_token,
505515
usage_rules=usage_rules,
506516
issued_at=issued_at,
@@ -517,3 +527,76 @@ def __init__(
517527
}
518528
self.exchange_request = exchange_request
519529
self.original_branch_id = original_branch_id
530+
531+
def payload_arguments(
532+
self,
533+
session_id: str,
534+
endpoint_context,
535+
item: SessionToken,
536+
claims_release_point: str,
537+
scope: Optional[dict] = None,
538+
extra_payload: Optional[dict] = None,
539+
secondary_identifier: str = "",
540+
) -> dict:
541+
"""
542+
:param session_id: Session ID
543+
:param endpoint_context: EndPoint Context
544+
:param claims_release_point: One of "userinfo", "introspection", "id_token", "access_token"
545+
:param scope: scope from the request
546+
:param extra_payload:
547+
:param secondary_identifier: Used if the claims returned are also based on rules for
548+
another release_point
549+
:param item: A SessionToken instance
550+
:type item: SessionToken
551+
:return: dictionary containing information to place in a token value
552+
"""
553+
payload = {}
554+
for _in, _out in [("scope", "scope"), ("resources", "aud")]:
555+
_val = getattr(item, _in)
556+
if _val:
557+
payload[_out] = _val
558+
else:
559+
_val = getattr(self, _in)
560+
if _val:
561+
payload[_out] = _val
562+
563+
payload["jti"] = uuid1().hex
564+
565+
if scope is None:
566+
scope = self.scope
567+
568+
payload = {"scope": scope, "aud": self.resources, "jti": uuid1().hex}
569+
570+
if extra_payload:
571+
payload.update(extra_payload)
572+
573+
_jkt = self.extra.get("dpop_jkt")
574+
if _jkt:
575+
payload["cnf"] = {"jkt": _jkt}
576+
577+
if self.exchange_request:
578+
client_id = self.exchange_request.get("client_id")
579+
if client_id:
580+
payload.update({"client_id": client_id, "sub": self.sub})
581+
582+
if item.claims:
583+
_claims_restriction = item.claims
584+
else:
585+
_claims_restriction = endpoint_context.claims_interface.get_claims(
586+
session_id,
587+
scopes=scope,
588+
claims_release_point=claims_release_point,
589+
secondary_identifier=secondary_identifier,
590+
)
591+
592+
user_id, _, _ = endpoint_context.session_manager.decrypt_session_id(session_id)
593+
user_info = endpoint_context.claims_interface.get_user_claims(user_id, _claims_restriction)
594+
payload.update(user_info)
595+
596+
# Should I add the acr value
597+
if self.add_acr_value(claims_release_point):
598+
payload["acr"] = self.authentication_event["authn_info"]
599+
elif self.add_acr_value(secondary_identifier):
600+
payload["acr"] = self.authentication_event["authn_info"]
601+
602+
return payload

src/idpyoidc/server/session/manager.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,7 @@ def create_grant(
223223
def create_exchange_grant(
224224
self,
225225
exchange_request: TokenExchangeRequest,
226+
original_grant: Grant,
226227
original_session_id: str,
227228
user_id: str,
228229
client_id: Optional[str] = "",
@@ -241,11 +242,13 @@ def create_exchange_grant(
241242
"""
242243

243244
return self.add_exchange_grant(
245+
authentication_event=original_grant.authentication_event,
246+
authorization_request=original_grant.authorization_request,
244247
exchange_request=exchange_request,
245248
original_branch_id=original_session_id,
246249
path=self.make_path(user_id=user_id, client_id=client_id),
250+
sub=original_grant.sub,
247251
token_usage_rules=token_usage_rules,
248-
sub=self.sub_func[sub_type](user_id, salt=self.get_salt(), sector_identifier=""),
249252
scope=scopes
250253
)
251254

@@ -286,6 +289,7 @@ def create_session(
286289
def create_exchange_session(
287290
self,
288291
exchange_request: TokenExchangeRequest,
292+
original_grant: Grant,
289293
original_session_id: str,
290294
user_id: str,
291295
client_id: Optional[str] = "",
@@ -309,6 +313,7 @@ def create_exchange_session(
309313

310314
return self.create_exchange_grant(
311315
exchange_request=exchange_request,
316+
original_grant=original_grant,
312317
original_session_id=original_session_id,
313318
user_id=user_id,
314319
client_id=client_id,

0 commit comments

Comments
 (0)