Skip to content
This repository was archived by the owner on Jun 23, 2023. It is now read-only.

Commit b40c5bc

Browse files
committed
Add support of refresh token on token exchange
Support audience for refresh token
1 parent 77251f1 commit b40c5bc

File tree

5 files changed

+239
-55
lines changed

5 files changed

+239
-55
lines changed

docs/source/contents/conf.rst

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ An example::
178178
- implicit
179179
- urn:ietf:params:oauth:grant-type:jwt-bearer
180180
- refresh_token
181+
- urn:ietf:params:oauth:grant-type:token-exchange
181182
claim_types_supported:
182183
- normal
183184
- aggregated
@@ -486,7 +487,8 @@ An example::
486487
"supports_minting": ["access_token", "refresh_token"]
487488
}
488489
},
489-
"expires_in": 43200
490+
"expires_in": 43200,
491+
"audience": ['https://www.example.com']
490492
}
491493
}
492494
},

docs/source/contents/usage.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,3 +125,44 @@ oidc-op will return a json response like this::
125125
"oLyRj7sJJ3XvAYjeDCe8rQ"
126126
]
127127
}
128+
129+
Token exchange
130+
-------------
131+
132+
Here an example about how to exchange an access token for a new access token.
133+
134+
import requests
135+
136+
CLIENT_ID = "DBP60x3KUQfCYWZlqFaS_Q"
137+
CLIENT_SECRET="8526270403788522b2444e87ea90c53bcafb984119cec92eeccc12f1"
138+
SUBJECT_TOKEN="Z0FBQUFkF3czZRU...BfdTJkQXlCSm55cVpxQ1A0Y0RkWEtQTT0="
139+
REQUESTED_TOKEN_TYPE="urn:ietf:params:oauth:token-type:access_token"
140+
141+
data = {
142+
"grant_type" : "urn:ietf:params:oauth:grant-type:token-exchange",
143+
"requested_token_type" : f"{REQUESTED_TOKEN_TYPE}",
144+
"client_id" : f"{CLIENT_ID}",
145+
"client_secret" : f"{CLIENT_SECRET}",
146+
"subject_token" : f"{SUBJECT_TOKEN}"
147+
}
148+
headers = {'Content-Type': "application/x-www-form-urlencoded" }
149+
response = requests.post(
150+
'https://snf-19725.ok-kno.grnetcloud.net/OIDC/token', verify=False, data=data, headers=headers
151+
)
152+
153+
oidc-op will return a json response like this::
154+
155+
{
156+
"access_token": "eyJhbGciOiJFUzI1NiIsI...Bo6aQcOKEN-1U88jjKxLb-9Q",
157+
"scope": "openid email",
158+
"issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
159+
"expires_in": 86400
160+
}
161+
162+
In order to request a refresh token the value of `requested_token_type` should be set to
163+
`urn:ietf:params:oauth:token-type:refresh_token`.
164+
165+
The [RFC-8693](https://datatracker.ietf.org/doc/html/rfc8693) describes the `audience` parameter that
166+
defines the authorized targets of a token exchange request.
167+
If `subject_token = urn:ietf:params:oauth:token-type:refresh_token` then `audience` should not be
168+
included in the token exchange request.

docs/source/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ Idpy OIDC-op implements the following standards:
2121
* `OpenID Connect Back-Channel Logout 1.0 <https://openid.net/specs/openid-connect-backchannel-1_0.html>`_
2222
* `OpenID Connect Front-Channel Logout 1.0 <https://openid.net/specs/openid-connect-frontchannel-1_0.html>`_
2323
* `OAuth2 Token introspection <https://tools.ietf.org/html/rfc7662>`_
24+
* `OAuth2 Token exchange <https://datatracker.ietf.org/doc/html/rfc8693>`_
2425

2526

2627
It also comes with the following `add_on` modules.

src/oidcop/oidc/token.py

Lines changed: 121 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from oidcop.token.exception import UnknownToken
2525
from oidcop.exception import UnAuthorizedClientScope, ToOld
2626
from oidcop.session.token import AccessToken
27+
from oidcop.authn_event import create_authn_event
2728

2829
logger = logging.getLogger(__name__)
2930

@@ -207,7 +208,7 @@ def post_parse_request(
207208

208209

209210
class RefreshTokenHelper(TokenEndpointHelper):
210-
def process_request(self, req: Union[Message, dict], **kwargs):
211+
def process_request(self, req: Union[Message, dict], **kwargs):
211212
_context = self.endpoint.server_get("endpoint_context")
212213
_mngr = _context.session_manager
213214

@@ -216,6 +217,8 @@ def process_request(self, req: Union[Message, dict], **kwargs):
216217

217218
token_value = req["refresh_token"]
218219
_session_info = _mngr.get_session_info_by_token(token_value, grant=True)
220+
grant = _session_info["grant"]
221+
audience = grant.authorization_request.get("audience", {})
219222
if _session_info["client_id"] != req["client_id"]:
220223
logger.debug("{} owner of token".format(_session_info["client_id"]))
221224
logger.warning("{} using token it was not given".format(req["client_id"]))
@@ -377,7 +380,7 @@ def __init__(self, endpoint, config=None):
377380
"urn:ietf:params:oauth:token-type:access_token",
378381
"urn:ietf:params:oauth:token-type:jwt",
379382
# "urn:ietf:params:oauth:token-type:id_token",
380-
# "urn:ietf:params:oauth:token-type:refresh_token",
383+
"urn:ietf:params:oauth:token-type:refresh_token",
381384
]
382385

383386
def post_parse_request(self, request, client_id="", **kwargs):
@@ -423,7 +426,7 @@ def post_parse_request(self, request, client_id="", **kwargs):
423426

424427
token = _mngr.find_token(_session_info["session_id"], request["subject_token"])
425428

426-
if not isinstance(token, AccessToken):
429+
if not isinstance(token, (AccessToken, RefreshToken)):
427430
return self.error_cls(
428431
error="invalid_request", error_description="Wrong token type"
429432
)
@@ -436,6 +439,14 @@ def post_parse_request(self, request, client_id="", **kwargs):
436439

437440
def check_for_errors(self, request):
438441
_context = self.endpoint.server_get("endpoint_context")
442+
443+
#TODO: also check if the (valid) subject_token matches subject_token_type
444+
if request["subject_token_type"] not in self.token_types_allowed:
445+
return TokenErrorResponse(
446+
error="invalid_request",
447+
error_description="Unsupported subject token type",
448+
)
449+
439450
if "resource" in request:
440451
iss = urlparse(_context.issuer)
441452
if any(
@@ -446,9 +457,13 @@ def check_for_errors(self, request):
446457
)
447458

448459
if "audience" in request:
449-
if any(
450-
aud != _context.issuer for aud in request["audience"]
451-
):
460+
if request["subject_token_type"] == "urn:ietf:params:oauth:token-type:refresh_token":
461+
return TokenErrorResponse(
462+
error="invalid_target", error_description="Refresh token has single owner"
463+
)
464+
_token_usage_rules = _context.authz.usage_rules(request["client_id"])
465+
audience = _token_usage_rules["refresh_token"].get("audience", {})
466+
if (not len(set(request["audience"]).intersection(set(audience)))):
452467
return TokenErrorResponse(
453468
error="invalid_target", error_description="Unknown audience"
454469
)
@@ -468,21 +483,18 @@ def check_for_errors(self, request):
468483
error="invalid_request", error_description="Actor token not supported"
469484
)
470485

471-
# TODO: also check if the (valid) subject_token matches subject_token_type
472-
if request["subject_token_type"] not in self.token_types_allowed:
473-
return TokenErrorResponse(
474-
error="invalid_request",
475-
error_description="Unsupported subject token type",
476-
)
477-
478-
def token_exchange_response(self, token):
479-
response_args = {
480-
"issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
481-
"token_type": token.token_type,
482-
"access_token": token.value,
483-
"scope": token.scope,
484-
"expires_in": token.usage_rules["expires_in"]
485-
}
486+
def token_exchange_response(self, access_token, refresh_token=None):
487+
response_args = {}
488+
response_args["access_token"] = access_token.value
489+
response_args["scope"] = access_token.scope
490+
if refresh_token is None:
491+
response_args["issued_token_type"] = "urn:ietf:params:oauth:token-type:access_token"
492+
response_args["token_type"] = access_token.token_type
493+
response_args["expires_in"] = access_token.usage_rules["expires_in"]
494+
else:
495+
response_args["issued_token_type"] = "urn:ietf:params:oauth:token-type:refresh_token"
496+
response_args["refresh_token"] = refresh_token.value
497+
response_args["expires_in"] = refresh_token.usage_rules["expires_in"]
486498
return TokenExchangeResponse(**response_args)
487499

488500
def process_request(self, request, **kwargs):
@@ -516,7 +528,7 @@ def process_request(self, request, **kwargs):
516528

517529
token = _mngr.find_token(_session_info["session_id"], request["subject_token"])
518530

519-
if not isinstance(token, AccessToken):
531+
if not isinstance(token, (AccessToken, RefreshToken)):
520532
return self.error_cls(
521533
error="invalid_request", error_description="Wrong token type"
522534
)
@@ -538,27 +550,96 @@ def process_request(self, request, **kwargs):
538550
error_description="Unauthorized scope requested",
539551
)
540552

541-
try:
542-
new_token = self._mint_token(
543-
token_class=token.token_class,
544-
grant=grant,
545-
session_id=_session_info["session_id"],
553+
_requested_token_type = request.get("requested_token_type",
554+
"urn:ietf:params:oauth:token-type:access_token")
555+
if (
556+
_requested_token_type == "urn:ietf:params:oauth:token-type:access_token"
557+
or _requested_token_type == "urn:ietf:params:oauth:token-type:id_token"
558+
):
559+
_token_class = _requested_token_type.split(":")[-1]
560+
_token_type = token.token_type
561+
562+
try:
563+
new_token = self._mint_token(
564+
token_class=_token_class,
565+
grant=grant,
566+
session_id=_session_info["session_id"],
567+
client_id=request["client_id"],
568+
based_on=token,
569+
scope=request.get("scope"),
570+
token_args={
571+
"resources":request.get("resource"),
572+
},
573+
token_type=_token_type
574+
)
575+
except MintingNotAllowed:
576+
logger.error(f"Minting not allowed for {_token_class}")
577+
return self.error_cls(
578+
error="invalid_grant",
579+
error_description="Token Exchange not allowed with that token",
580+
)
581+
582+
return self.token_exchange_response(access_token=new_token)
583+
584+
elif _requested_token_type == "urn:ietf:params:oauth:token-type:refresh_token":
585+
_token_class = "refresh_token"
586+
_token_type = None
587+
authn_event = create_authn_event(_session_info["user_id"])
588+
_token_usage_rules = _context.authz.usage_rules(request["client_id"])
589+
_exp_in = _token_usage_rules["refresh_token"].get("expires_in")
590+
if _exp_in and "valid_until" in authn_event:
591+
authn_event["valid_until"] = utc_time_sans_frac() + _exp_in
592+
593+
sid = _mngr.create_session(
594+
authn_event=authn_event,
595+
auth_req=request,
596+
user_id=_session_info["user_id"],
546597
client_id=request["client_id"],
547-
based_on=token,
548-
scope=request.get("scope"),
549-
token_args={
550-
"resources":request.get("resource"),
551-
},
552-
token_type=token.token_type
553-
)
554-
except MintingNotAllowed:
555-
logger.error("Minting not allowed for 'access_token'")
556-
return self.error_cls(
557-
error="invalid_grant",
558-
error_description="Token Exchange not allowed with that token",
598+
token_usage_rules=_token_usage_rules,
559599
)
560600

561-
return self.token_exchange_response(token=new_token)
601+
try:
602+
new_token = self._mint_token(
603+
token_class="access_token",
604+
grant=_mngr.get_grant(sid),
605+
session_id=sid,
606+
client_id=request["client_id"],
607+
based_on=token,
608+
scope=request.get("scope"),
609+
token_args={
610+
"resources":request.get("resource"),
611+
},
612+
token_type=_token_type
613+
)
614+
except MintingNotAllowed:
615+
logger.error("Minting not allowed for 'access_token'")
616+
return self.error_cls(
617+
error="invalid_grant",
618+
error_description="Token Exchange not allowed with that token",
619+
)
620+
621+
try:
622+
refresh_token = self._mint_token(
623+
token_class=_token_class,
624+
grant=_mngr.get_grant(sid),
625+
session_id=sid,
626+
client_id=request["client_id"],
627+
based_on=token,
628+
scope=request.get("scope"),
629+
token_args={
630+
"resources":request.get("resource"),
631+
},
632+
token_type=_token_type
633+
)
634+
except MintingNotAllowed:
635+
logger.error("Minting not allowed for 'refresh_token'")
636+
return self.error_cls(
637+
error="invalid_grant",
638+
error_description="Token Exchange not allowed with that token",
639+
)
640+
641+
return self.token_exchange_response(access_token=new_token,
642+
refresh_token=refresh_token)
562643

563644
class Token(oauth2.token.Token):
564645
request_cls = Message

0 commit comments

Comments
 (0)