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

Commit

Permalink
Move token exchange to oauth2
Browse files Browse the repository at this point in the history
  • Loading branch information
ctriant committed Jan 19, 2022
1 parent 9e03268 commit dd1e2c4
Show file tree
Hide file tree
Showing 6 changed files with 292 additions and 290 deletions.
2 changes: 1 addition & 1 deletion docs/source/contents/conf.rst
Original file line number Diff line number Diff line change
Expand Up @@ -677,7 +677,7 @@ defined for each token type supported.
```
"grant_types_supported":{
"urn:ietf:params:oauth:grant-type:token-exchange": {
"class": "oidcop.oidc.token.TokenExchangeHelper",
"class": "oidcop.oauth2.token.TokenExchangeHelper",
"kwargs": {
"subject_token_types_supported": [
"urn:ietf:params:oauth:token-type:access_token",
Expand Down
290 changes: 289 additions & 1 deletion src/oidcop/oauth2/token.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,28 @@
from typing import Union

from cryptojwt.jwe.exception import JWEException
from cryptojwt.exception import JWKESTException
from oidcmsg.message import Message
from oidcmsg.oauth2 import AccessTokenResponse
from oidcmsg.oauth2 import ResponseMessage
from oidcmsg.oauth2 import (TokenExchangeRequest, TokenExchangeResponse)
from oidcmsg.exception import MissingRequiredValue, MissingRequiredAttribute
from oidcmsg.oidc import RefreshAccessTokenRequest
from oidcmsg.oidc import TokenErrorResponse
from oidcmsg.time_util import utc_time_sans_frac

from oidcop import sanitize
from oidcop.constant import DEFAULT_TOKEN_LIFETIME
from oidcop.endpoint import Endpoint
from oidcop.exception import ProcessError
from oidcop.exception import ProcessError, UnAuthorizedClientScope, ToOld
from oidcop.session.grant import AuthorizationCode
from oidcop.session.grant import Grant
from oidcop.session.grant import RefreshToken
from oidcop.session.token import MintingNotAllowed
from oidcop.session.token import SessionToken
from oidcop.token.exception import UnknownToken
from oidcop.exception import ImproperlyConfigured
from oidcop.oauth2.authorization import check_unknown_scopes_policy
from oidcop.util import importer

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -358,6 +363,289 @@ def post_parse_request(

return request

class TokenExchangeHelper(TokenEndpointHelper):
"""Implements Token Exchange a.k.a. RFC8693"""

def __init__(self, endpoint, config=None):
TokenEndpointHelper.__init__(self, endpoint=endpoint, config=config)

if config is None:
self.config = {
"subject_token_types_supported": [
"urn:ietf:params:oauth:token-type:access_token",
"urn:ietf:params:oauth:token-type:refresh_token",
],
"requested_token_types_supported": [
"urn:ietf:params:oauth:token-type:access_token",
"urn:ietf:params:oauth:token-type:refresh_token",
],
"policy": {
"": {
"callable": default_token_exchange_policy,
"kwargs": {
"scope": ["openid"]
}
}
}
}
else:
self.config = config

self.subject_token_types_mapping = {
"urn:ietf:params:oauth:token-type:access_token": "access_token",
"urn:ietf:params:oauth:token-type:refresh_token": "refresh_token"
}

def post_parse_request(self, request, client_id="", **kwargs):
request = TokenExchangeRequest(**request.to_dict())

_context = self.endpoint.server_get("endpoint_context")
if "token_exchange" in _context.cdb[request["client_id"]]:
config = _context.cdb[request["client_id"]]["token_exchange"]
else:
config = self.config

try:
keyjar = _context.keyjar
except AttributeError:
keyjar = ""

try:
request.verify(keyjar=keyjar, opponent_id=client_id)
except (
MissingRequiredAttribute,
ValueError,
MissingRequiredValue,
JWKESTException,
) as err:
return self.endpoint.error_cls(
error="invalid_request", error_description="%s" % err
)

_mngr = _context.session_manager
try:
_session_info = _mngr.get_session_info_by_token(
request["subject_token"], grant=True
)
except (KeyError, UnknownToken):
logger.error("Subject token invalid.")
return self.error_cls(
error="invalid_request",
error_description="Subject token invalid"
)

token = _mngr.find_token(_session_info["session_id"], request["subject_token"])
if token.is_active() is False:
return self.error_cls(
error="invalid_request", error_description="Subject token inactive"
)

resp = self._enforce_policy(request, token, config)

return resp

def _enforce_policy(self, request, token, config):
_context = self.endpoint.server_get("endpoint_context")

subject_token_types_supported = config.get(
"subject_token_types_supported", self.subject_token_types_mapping.keys()
)
subject_token_type = request["subject_token_type"]
if subject_token_type not in subject_token_types_supported:
return TokenErrorResponse(
error="invalid_request",
error_description="Unsupported subject token type",
)
if self.subject_token_types_mapping[subject_token_type] != token.token_class:
return TokenErrorResponse(
error="invalid_request",
error_description="Wrong token type",
)

if (
"requested_token_type" in request
and request["requested_token_type"] not in config["requested_token_types_supported"]
):
return TokenErrorResponse(
error="invalid_request",
error_description="Unsupported requested token type",
)

request_info = dict(scope=request.get("scope", []))
try:
check_unknown_scopes_policy(request_info, request["client_id"], _context)
except UnAuthorizedClientScope:
return self.error_cls(
error="invalid_grant",
error_description="Unauthorized scope requested",
)

subject_token_type = request["subject_token_type"]
if subject_token_type not in config["policy"]:
if "" not in config["policy"]:
raise ImproperlyConfigured(
"subject_token_type {subject_token_type} missing from "
"policy and no default is defined"
)
subject_token_type = ""

policy = config["policy"][subject_token_type]
callable = policy["callable"]
kwargs = policy["kwargs"]

try:
if isinstance(callable, str):
fn = importer(callable)
else:
fn = callable
except Exception:
raise ImproperlyConfigured(
"Error importing {callable} policy callable"
)

try:
return fn(request, context=_context, subject_token=token, **kwargs)
except Exception:
logger.error("Error while executing the {fn} policy callable")
return self.error_cls(
error="server_error",
error_description="Internal server error"
)

def token_exchange_response(self, token):
response_args = {}
response_args["access_token"] = token.value
response_args["scope"] = token.scope
response_args["issued_token_type"] = token.token_class
response_args["expires_in"] = token.usage_rules.get("expires_in", 0)
if hasattr(token, "token_type"):
response_args["token_type"] = token.token_type
else:
response_args["token_type"] = "N_A"

return TokenExchangeResponse(**response_args)

def process_request(self, request, **kwargs):
_context = self.endpoint.server_get("endpoint_context")
_mngr = _context.session_manager
try:
_session_info = _mngr.get_session_info_by_token(
request["subject_token"], grant=True
)
except ToOld:
logger.error("Subject token has expired.")
return self.error_cls(
error="invalid_request",
error_description="Subject token has expired"
)
except (KeyError, UnknownToken):
logger.error("Subject token invalid.")
return self.error_cls(
error="invalid_request",
error_description="Subject token invalid"
)

token = _mngr.find_token(_session_info["session_id"], request["subject_token"])
_requested_token_type = request.get("requested_token_type",
"urn:ietf:params:oauth:token-type:access_token")

_token_class = _requested_token_type.split(":")[-1]
if _token_class == "access_token":
_token_type = _token_class
else:
_token_type = None

sid = _session_info["session_id"]
if request["client_id"] != _session_info["client_id"]:
_token_usage_rules = _context.authz.usage_rules(request["client_id"])

sid = _mngr.create_exchange_session(
exchange_request=request,
original_session_id=sid,
user_id=_session_info["user_id"],
client_id=request["client_id"],
token_usage_rules=_token_usage_rules,
)

try:
_session_info = _mngr.get_session_info(
session_id=sid, grant=True)
except Exception:
logger.error("Error retrieving token exchabge session information")
return self.error_cls(
error="server_error",
error_description="Internal server error"
)

try:
new_token = self._mint_token(
token_class=_token_class,
grant=_session_info["grant"],
session_id=sid,
client_id=request["client_id"],
based_on=token,
scope=request.get("scope"),
token_args={
"resources":request.get("resource"),
},
token_type=_token_type
)
except MintingNotAllowed:
logger.error(f"Minting not allowed for {_token_class}")
return self.error_cls(
error="invalid_grant",
error_description="Token Exchange not allowed with that token",
)

return self.token_exchange_response(token=new_token)

def default_token_exchange_policy(request, context, subject_token, **kwargs):
if "resource" in request:
resource = kwargs.get("resource", [])
if not resource:
pass
elif (not len(set(request["resource"]).intersection(set(resource)))):
return TokenErrorResponse(
error="invalid_target", error_description="Unknown resource"
)

if "audience" in request:
if request["subject_token_type"] == "urn:ietf:params:oauth:token-type:refresh_token":
return TokenErrorResponse(
error="invalid_target", error_description="Refresh token has single owner"
)
audience = kwargs.get("audience", [])
if not audience:
pass
elif (audience and not len(set(request["audience"]).intersection(set(audience)))):
return TokenErrorResponse(
error="invalid_target", error_description="Unknown audience"
)

if "actor_token" in request or "actor_token_type" in request:
return TokenErrorResponse(
error="invalid_request", error_description="Actor token not supported"
)

if (
"requested_token_type" in request
and request["requested_token_type"] == "urn:ietf:params:oauth:token-type:refresh_token"
):
if "offline_access" not in subject_token.scope:
return TokenErrorResponse(
error="invalid_request",
error_description=f"Exchange {request['subject_token_type']} to refresh token forbbiden",
)

scopes = list(set(request.get("scope", ["openid"])).intersection(kwargs.get("scope", ["openid"])))
if scopes:
request["scope"] = scopes
else:
return TokenErrorResponse(
error="invalid_request",
error_description="No supported scope requested",
)
return request

class Token(Endpoint):
request_cls = Message
Expand Down
Loading

0 comments on commit dd1e2c4

Please sign in to comment.