@@ -352,6 +352,173 @@ def post_parse_request(
352352
353353 return request
354354
355+ class TokenExchangeHelper (TokenEndpointHelper ):
356+ """Implements Token Exchange a.k.a. RFC8693"""
357+
358+ def __init__ (self , endpoint , config = None ):
359+ TokenEndpointHelper .__init__ (self , endpoint = endpoint , config = config )
360+
361+ # TODO: should we even have a policy for the simple use cases?
362+ if config is None :
363+ self .policy = {}
364+ else :
365+ self .policy = config .get ('policy' , {})
366+
367+ # TODO: Make this a part of the policy. Note the distinction between
368+ # requested_token_type, subject_token_type, actor_token_type, issued_token_type
369+ self .token_types_allowed = [
370+ "urn:ietf:params:oauth:token-type:access_token" ,
371+ "urn:ietf:params:oauth:token-type:jwt" ,
372+ # "urn:ietf:params:oauth:token-type:id_token",
373+ # "urn:ietf:params:oauth:token-type:refresh_token",
374+ ]
375+
376+ def post_parse_request (self , request , client_id = "" , ** kwargs ):
377+ request = TokenExchangeRequest (** request .to_dict ())
378+
379+ # if "client_id" not in request:
380+ # request["client_id"] = client_id
381+
382+ keyjar = getattr (self .endpoint_context , "keyjar" , "" )
383+
384+ try :
385+ request .verify (keyjar = keyjar , opponent_id = client_id )
386+ except (
387+ MissingRequiredAttribute ,
388+ ValueError ,
389+ MissingRequiredValue ,
390+ JWKESTException ,
391+ ) as err :
392+ return self .endpoint .error_cls (
393+ error = "invalid_request" , error_description = "%s" % err
394+ )
395+
396+
397+ error = self .check_for_errors (request = request )
398+ if error is not None :
399+ return error
400+
401+ _mngr = self .endpoint_context .session_manager
402+ try :
403+ _session_info = _mngr .get_session_info_by_token (
404+ request ["subject_token" ], grant = True
405+ )
406+ except (KeyError , UnknownToken ):
407+ logger .error ("Subject token invalid." )
408+ return self .error_cls (
409+ error = "invalid_request" ,
410+ error_description = "Subject token invalid"
411+ )
412+
413+ token = _mngr .find_token (_session_info ["session_id" ], request ["subject_token" ])
414+
415+ if not isinstance (token , AccessToken ):
416+ return self .error_cls (
417+ error = "invalid_request" , error_description = "Wrong token type"
418+ )
419+
420+ if token .is_active () is False :
421+ return self .error_cls (
422+ error = "invalid_request" , error_description = "Subject token inactive"
423+ )
424+
425+ return request
426+
427+ def check_for_errors (self , request ):
428+ context = self .endpoint .endpoint_context
429+ if "resource" in request :
430+ iss = urlparse (context .issuer )
431+ if any (
432+ urlparse (res ).netloc != iss .netloc for res in request ["resource" ]
433+ ):
434+ return TokenErrorResponse (
435+ error = "invalid_target" , error_description = "Unknown resource"
436+ )
437+
438+ if "audience" in request :
439+ if any (
440+ aud != context .issuer for aud in request ["audience" ]
441+ ):
442+ return TokenErrorResponse (
443+ error = "invalid_target" , error_description = "Unknown audience"
444+ )
445+
446+ # TODO: if requested type is jwt make sure our tokens are jwt
447+ if (
448+ "requested_token_type" in request
449+ and request ["requested_token_type" ] not in self .token_types_allowed
450+ ):
451+ return TokenErrorResponse (
452+ error = "invalid_target" ,
453+ error_description = "Unsupported requested token type"
454+ )
455+
456+ if "actor_token" in request or "actor_token_type" in request :
457+ return TokenErrorResponse (
458+ error = "invalid_request" , error_description = "Actor token not supported"
459+ )
460+
461+ # TODO: also check if the (valid) subject_token matches subject_token_type
462+ if request ["subject_token_type" ] not in self .token_types_allowed :
463+ return TokenErrorResponse (
464+ error = "invalid_request" ,
465+ error_description = "Unsupported subject token type" ,
466+ )
467+
468+ def token_exchange_response (self , token ):
469+ response_args = {
470+ "issued_token_type" : "urn:ietf:params:oauth:token-type:access_token" ,
471+ "token_type" : token .type ,
472+ "access_token" : token .value ,
473+ "scope" : token .scope ,
474+ "expires_in" : token .usage_rules ["expires_in" ]
475+ }
476+ return TokenExchangeResponse (** response_args )
477+
478+ def process_request (self , request , ** kwargs ):
479+ # TODO: should we even have a policy for the simple use cases?
480+ # client_policy = self.policy.get(req["client_id"]) or self.policy.get("default")
481+ # if not client_policy:
482+ # logger.error(
483+ # "TokenExchange policy for client {req['client_id']} or default missing."
484+ # )
485+ # return TokenErrorResponse(
486+ # error="invalid_request", error_description="Not allowed"
487+ # )
488+ _mngr = self .endpoint_context .session_manager
489+ try :
490+ _session_info = _mngr .get_session_info_by_token (
491+ request ["subject_token" ],
492+ grant = True ,
493+ )
494+ except KeyError :
495+ logger .error ("Subject token invalid." )
496+ return self .error_cls (
497+ error = "invalid_grant" ,
498+ error_description = "Subject token invalid" ,
499+ )
500+
501+ token = _mngr .find_token (_session_info ["session_id" ], request ["subject_token" ])
502+ grant = _session_info ["grant" ]
503+
504+ try :
505+ new_token = grant .mint_token (
506+ session_id = _session_info ["session_id" ],
507+ endpoint_context = self .endpoint_context ,
508+ token_type = 'access_token' ,
509+ token_handler = _mngr .token_handler ["access_token" ],
510+ based_on = token ,
511+ resources = request .get ("resource" ),
512+ scope = request .get ("scope" ),
513+ )
514+ except MintingNotAllowed :
515+ logger .error ("Minting not allowed for 'access_token'" )
516+ return self .error_cls (
517+ error = "invalid_grant" ,
518+ error_description = "Token Exchange not allowed with that token" ,
519+ )
520+
521+ return self .token_exchange_response (token = new_token )
355522
356523class Token (oauth2 .token .Token ):
357524 request_cls = Message
@@ -367,4 +534,5 @@ class Token(oauth2.token.Token):
367534 helper_by_grant_type = {
368535 "authorization_code" : AccessTokenHelper ,
369536 "refresh_token" : RefreshTokenHelper ,
537+ "urn:ietf:params:oauth:grant-type:token-exchange" : TokenExchangeHelper ,
370538 }
0 commit comments