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

Commit dd1e2c4

Browse files
committed
Move token exchange to oauth2
1 parent 9e03268 commit dd1e2c4

File tree

6 files changed

+292
-290
lines changed

6 files changed

+292
-290
lines changed

docs/source/contents/conf.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -677,7 +677,7 @@ defined for each token type supported.
677677
```
678678
"grant_types_supported":{
679679
"urn:ietf:params:oauth:grant-type:token-exchange": {
680-
"class": "oidcop.oidc.token.TokenExchangeHelper",
680+
"class": "oidcop.oauth2.token.TokenExchangeHelper",
681681
"kwargs": {
682682
"subject_token_types_supported": [
683683
"urn:ietf:params:oauth:token-type:access_token",

src/oidcop/oauth2/token.py

+289-1
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,28 @@
33
from typing import Union
44

55
from cryptojwt.jwe.exception import JWEException
6+
from cryptojwt.exception import JWKESTException
67
from oidcmsg.message import Message
78
from oidcmsg.oauth2 import AccessTokenResponse
89
from oidcmsg.oauth2 import ResponseMessage
10+
from oidcmsg.oauth2 import (TokenExchangeRequest, TokenExchangeResponse)
11+
from oidcmsg.exception import MissingRequiredValue, MissingRequiredAttribute
912
from oidcmsg.oidc import RefreshAccessTokenRequest
1013
from oidcmsg.oidc import TokenErrorResponse
1114
from oidcmsg.time_util import utc_time_sans_frac
1215

1316
from oidcop import sanitize
1417
from oidcop.constant import DEFAULT_TOKEN_LIFETIME
1518
from oidcop.endpoint import Endpoint
16-
from oidcop.exception import ProcessError
19+
from oidcop.exception import ProcessError, UnAuthorizedClientScope, ToOld
1720
from oidcop.session.grant import AuthorizationCode
1821
from oidcop.session.grant import Grant
1922
from oidcop.session.grant import RefreshToken
2023
from oidcop.session.token import MintingNotAllowed
2124
from oidcop.session.token import SessionToken
2225
from oidcop.token.exception import UnknownToken
26+
from oidcop.exception import ImproperlyConfigured
27+
from oidcop.oauth2.authorization import check_unknown_scopes_policy
2328
from oidcop.util import importer
2429

2530
logger = logging.getLogger(__name__)
@@ -358,6 +363,289 @@ def post_parse_request(
358363

359364
return request
360365

366+
class TokenExchangeHelper(TokenEndpointHelper):
367+
"""Implements Token Exchange a.k.a. RFC8693"""
368+
369+
def __init__(self, endpoint, config=None):
370+
TokenEndpointHelper.__init__(self, endpoint=endpoint, config=config)
371+
372+
if config is None:
373+
self.config = {
374+
"subject_token_types_supported": [
375+
"urn:ietf:params:oauth:token-type:access_token",
376+
"urn:ietf:params:oauth:token-type:refresh_token",
377+
],
378+
"requested_token_types_supported": [
379+
"urn:ietf:params:oauth:token-type:access_token",
380+
"urn:ietf:params:oauth:token-type:refresh_token",
381+
],
382+
"policy": {
383+
"": {
384+
"callable": default_token_exchange_policy,
385+
"kwargs": {
386+
"scope": ["openid"]
387+
}
388+
}
389+
}
390+
}
391+
else:
392+
self.config = config
393+
394+
self.subject_token_types_mapping = {
395+
"urn:ietf:params:oauth:token-type:access_token": "access_token",
396+
"urn:ietf:params:oauth:token-type:refresh_token": "refresh_token"
397+
}
398+
399+
def post_parse_request(self, request, client_id="", **kwargs):
400+
request = TokenExchangeRequest(**request.to_dict())
401+
402+
_context = self.endpoint.server_get("endpoint_context")
403+
if "token_exchange" in _context.cdb[request["client_id"]]:
404+
config = _context.cdb[request["client_id"]]["token_exchange"]
405+
else:
406+
config = self.config
407+
408+
try:
409+
keyjar = _context.keyjar
410+
except AttributeError:
411+
keyjar = ""
412+
413+
try:
414+
request.verify(keyjar=keyjar, opponent_id=client_id)
415+
except (
416+
MissingRequiredAttribute,
417+
ValueError,
418+
MissingRequiredValue,
419+
JWKESTException,
420+
) as err:
421+
return self.endpoint.error_cls(
422+
error="invalid_request", error_description="%s" % err
423+
)
424+
425+
_mngr = _context.session_manager
426+
try:
427+
_session_info = _mngr.get_session_info_by_token(
428+
request["subject_token"], grant=True
429+
)
430+
except (KeyError, UnknownToken):
431+
logger.error("Subject token invalid.")
432+
return self.error_cls(
433+
error="invalid_request",
434+
error_description="Subject token invalid"
435+
)
436+
437+
token = _mngr.find_token(_session_info["session_id"], request["subject_token"])
438+
if token.is_active() is False:
439+
return self.error_cls(
440+
error="invalid_request", error_description="Subject token inactive"
441+
)
442+
443+
resp = self._enforce_policy(request, token, config)
444+
445+
return resp
446+
447+
def _enforce_policy(self, request, token, config):
448+
_context = self.endpoint.server_get("endpoint_context")
449+
450+
subject_token_types_supported = config.get(
451+
"subject_token_types_supported", self.subject_token_types_mapping.keys()
452+
)
453+
subject_token_type = request["subject_token_type"]
454+
if subject_token_type not in subject_token_types_supported:
455+
return TokenErrorResponse(
456+
error="invalid_request",
457+
error_description="Unsupported subject token type",
458+
)
459+
if self.subject_token_types_mapping[subject_token_type] != token.token_class:
460+
return TokenErrorResponse(
461+
error="invalid_request",
462+
error_description="Wrong token type",
463+
)
464+
465+
if (
466+
"requested_token_type" in request
467+
and request["requested_token_type"] not in config["requested_token_types_supported"]
468+
):
469+
return TokenErrorResponse(
470+
error="invalid_request",
471+
error_description="Unsupported requested token type",
472+
)
473+
474+
request_info = dict(scope=request.get("scope", []))
475+
try:
476+
check_unknown_scopes_policy(request_info, request["client_id"], _context)
477+
except UnAuthorizedClientScope:
478+
return self.error_cls(
479+
error="invalid_grant",
480+
error_description="Unauthorized scope requested",
481+
)
482+
483+
subject_token_type = request["subject_token_type"]
484+
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+
)
490+
subject_token_type = ""
491+
492+
policy = config["policy"][subject_token_type]
493+
callable = policy["callable"]
494+
kwargs = policy["kwargs"]
495+
496+
try:
497+
if isinstance(callable, str):
498+
fn = importer(callable)
499+
else:
500+
fn = callable
501+
except Exception:
502+
raise ImproperlyConfigured(
503+
"Error importing {callable} policy callable"
504+
)
505+
506+
try:
507+
return fn(request, context=_context, subject_token=token, **kwargs)
508+
except Exception:
509+
logger.error("Error while executing the {fn} policy callable")
510+
return self.error_cls(
511+
error="server_error",
512+
error_description="Internal server error"
513+
)
514+
515+
def token_exchange_response(self, token):
516+
response_args = {}
517+
response_args["access_token"] = token.value
518+
response_args["scope"] = token.scope
519+
response_args["issued_token_type"] = token.token_class
520+
response_args["expires_in"] = token.usage_rules.get("expires_in", 0)
521+
if hasattr(token, "token_type"):
522+
response_args["token_type"] = token.token_type
523+
else:
524+
response_args["token_type"] = "N_A"
525+
526+
return TokenExchangeResponse(**response_args)
527+
528+
def process_request(self, request, **kwargs):
529+
_context = self.endpoint.server_get("endpoint_context")
530+
_mngr = _context.session_manager
531+
try:
532+
_session_info = _mngr.get_session_info_by_token(
533+
request["subject_token"], grant=True
534+
)
535+
except ToOld:
536+
logger.error("Subject token has expired.")
537+
return self.error_cls(
538+
error="invalid_request",
539+
error_description="Subject token has expired"
540+
)
541+
except (KeyError, UnknownToken):
542+
logger.error("Subject token invalid.")
543+
return self.error_cls(
544+
error="invalid_request",
545+
error_description="Subject token invalid"
546+
)
547+
548+
token = _mngr.find_token(_session_info["session_id"], request["subject_token"])
549+
_requested_token_type = request.get("requested_token_type",
550+
"urn:ietf:params:oauth:token-type:access_token")
551+
552+
_token_class = _requested_token_type.split(":")[-1]
553+
if _token_class == "access_token":
554+
_token_type = _token_class
555+
else:
556+
_token_type = None
557+
558+
sid = _session_info["session_id"]
559+
if request["client_id"] != _session_info["client_id"]:
560+
_token_usage_rules = _context.authz.usage_rules(request["client_id"])
561+
562+
sid = _mngr.create_exchange_session(
563+
exchange_request=request,
564+
original_session_id=sid,
565+
user_id=_session_info["user_id"],
566+
client_id=request["client_id"],
567+
token_usage_rules=_token_usage_rules,
568+
)
569+
570+
try:
571+
_session_info = _mngr.get_session_info(
572+
session_id=sid, grant=True)
573+
except Exception:
574+
logger.error("Error retrieving token exchabge session information")
575+
return self.error_cls(
576+
error="server_error",
577+
error_description="Internal server error"
578+
)
579+
580+
try:
581+
new_token = self._mint_token(
582+
token_class=_token_class,
583+
grant=_session_info["grant"],
584+
session_id=sid,
585+
client_id=request["client_id"],
586+
based_on=token,
587+
scope=request.get("scope"),
588+
token_args={
589+
"resources":request.get("resource"),
590+
},
591+
token_type=_token_type
592+
)
593+
except MintingNotAllowed:
594+
logger.error(f"Minting not allowed for {_token_class}")
595+
return self.error_cls(
596+
error="invalid_grant",
597+
error_description="Token Exchange not allowed with that token",
598+
)
599+
600+
return self.token_exchange_response(token=new_token)
601+
602+
def default_token_exchange_policy(request, context, subject_token, **kwargs):
603+
if "resource" in request:
604+
resource = kwargs.get("resource", [])
605+
if not resource:
606+
pass
607+
elif (not len(set(request["resource"]).intersection(set(resource)))):
608+
return TokenErrorResponse(
609+
error="invalid_target", error_description="Unknown resource"
610+
)
611+
612+
if "audience" in request:
613+
if request["subject_token_type"] == "urn:ietf:params:oauth:token-type:refresh_token":
614+
return TokenErrorResponse(
615+
error="invalid_target", error_description="Refresh token has single owner"
616+
)
617+
audience = kwargs.get("audience", [])
618+
if not audience:
619+
pass
620+
elif (audience and not len(set(request["audience"]).intersection(set(audience)))):
621+
return TokenErrorResponse(
622+
error="invalid_target", error_description="Unknown audience"
623+
)
624+
625+
if "actor_token" in request or "actor_token_type" in request:
626+
return TokenErrorResponse(
627+
error="invalid_request", error_description="Actor token not supported"
628+
)
629+
630+
if (
631+
"requested_token_type" in request
632+
and request["requested_token_type"] == "urn:ietf:params:oauth:token-type:refresh_token"
633+
):
634+
if "offline_access" not in subject_token.scope:
635+
return TokenErrorResponse(
636+
error="invalid_request",
637+
error_description=f"Exchange {request['subject_token_type']} to refresh token forbbiden",
638+
)
639+
640+
scopes = list(set(request.get("scope", ["openid"])).intersection(kwargs.get("scope", ["openid"])))
641+
if scopes:
642+
request["scope"] = scopes
643+
else:
644+
return TokenErrorResponse(
645+
error="invalid_request",
646+
error_description="No supported scope requested",
647+
)
648+
return request
361649

362650
class Token(Endpoint):
363651
request_cls = Message

0 commit comments

Comments
 (0)