Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 31 additions & 29 deletions src/idpyoidc/server/client_authn.py
Original file line number Diff line number Diff line change
Expand Up @@ -262,14 +262,15 @@ def _verify(
request: Optional[Union[dict, Message]] = None,
authorization_token: Optional[str] = None,
endpoint=None, # Optional[Endpoint]
get_client_id_from_token=None,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this a pointer to a function/method?

wouldn't it be better Optional[...]?

**kwargs,
):
_token = request.get("access_token")
if _token is None:
raise ClientAuthenticationError("No access token")

res = {"token": _token}
_client_id = request.get("client_id")
_client_id = get_client_id_from_token(endpoint_context, _token, request)
if _client_id:
res["client_id"] = _client_id
return res
Expand Down Expand Up @@ -483,6 +484,7 @@ def verify_client(

auth_info = {}
methods = endpoint_context.client_authn_method
client_id = None
allowed_methods = getattr(endpoint, "client_authn_method")
if not allowed_methods:
allowed_methods = list(methods.keys())
Expand All @@ -499,48 +501,48 @@ def verify_client(
endpoint=endpoint,
get_client_id_from_token=get_client_id_from_token,
)
break
except (BearerTokenAuthenticationError, ClientAuthenticationError):
raise
except Exception as err:
logger.info("Verifying auth using {} failed: {}".format(_method.tag, err))
continue

if auth_info.get("method") == "none":
return auth_info
if auth_info.get("method") == "none" and auth_info.get("client_id") is None:
# For testing purposes only
break

client_id = auth_info.get("client_id")
if client_id is None:
raise ClientAuthenticationError("Failed to verify client")
client_id = auth_info.get("client_id")
if client_id is None:
raise ClientAuthenticationError("Failed to verify client")

if also_known_as:
client_id = also_known_as[client_id]
auth_info["client_id"] = client_id
if also_known_as:
client_id = also_known_as[client_id]
auth_info["client_id"] = client_id

if client_id not in endpoint_context.cdb:
raise UnknownClient("Unknown Client ID")
if client_id not in endpoint_context.cdb:
raise UnknownClient("Unknown Client ID")

_cinfo = endpoint_context.cdb[client_id]
_cinfo = endpoint_context.cdb[client_id]

if not valid_client_info(_cinfo):
logger.warning("Client registration has timed out or " "client secret is expired.")
raise InvalidClient("Not valid client")
if not valid_client_info(_cinfo):
logger.warning("Client registration has timed out or " "client secret is expired.")
raise InvalidClient("Not valid client")

# Validate that the used method is allowed for this client/endpoint
client_allowed_methods = _cinfo.get(
f"{endpoint.endpoint_name}_client_authn_method", _cinfo.get("client_authn_method")
)
if client_allowed_methods is not None and _method and _method.tag not in client_allowed_methods:
logger.info(
f"Allowed methods for client: {client_id} at endpoint: {endpoint.name} are: "
f"`{', '.join(client_allowed_methods)}`"
)
raise UnAuthorizedClient(
f"Authentication method: {_method.tag} not allowed for client: {client_id} in "
f"endpoint: {endpoint.name}"
# Validate that the used method is allowed for this client/endpoint
client_allowed_methods = _cinfo.get(
f"{endpoint.endpoint_name}_client_authn_method", _cinfo.get("client_authn_method")
)
if client_allowed_methods is not None and auth_info["method"] not in client_allowed_methods:
logger.info(
f"Allowed methods for client: {client_id} at endpoint: {endpoint.name} are: "
f"`{', '.join(client_allowed_methods)}`"
)
auth_info = {}
continue
break

# store what authn method was used
if auth_info.get("method"):
if "method" in auth_info and client_id:
_request_type = request.__class__.__name__
_used_authn_method = _cinfo.get("auth_method")
if _used_authn_method:
Expand Down
5 changes: 5 additions & 0 deletions src/idpyoidc/server/endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,9 @@ def set_client_authn_methods(self, **kwargs):
kwargs[self.auth_method_attribute] = _methods
elif _methods is not None: # [] or '' or something not None but regarded as nothing.
self.client_authn_method = ["none"] # Ignore default value
elif self.default_capabilities:
self.client_authn_method = self.default_capabilities.get("client_authn_method")
self.endpoint_info = construct_provider_info(self.default_capabilities, **kwargs)
return kwargs

def get_provider_info_attributes(self):
Expand Down Expand Up @@ -249,6 +252,8 @@ def client_authentication(self, request: Message, http_info: Optional[dict] = No
if authn_info == {} and self.client_authn_method and len(self.client_authn_method):
LOGGER.debug("client_authn_method: %s", self.client_authn_method)
raise UnAuthorizedClient("Authorization failed")
if "client_id" not in authn_info and authn_info.get("method") != "none":
raise UnAuthorizedClient("Authorization failed")

return authn_info

Expand Down
3 changes: 2 additions & 1 deletion src/idpyoidc/server/oauth2/introspection.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from idpyoidc.server.endpoint import Endpoint
from idpyoidc.server.token.exception import UnknownToken
from idpyoidc.server.token.exception import WrongTokenClass
from idpyoidc.server.exception import ToOld

LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -103,7 +104,7 @@ def process_request(self, request=None, release: Optional[list] = None, **kwargs
_session_info = _context.session_manager.get_session_info_by_token(
request_token, grant=True
)
except (UnknownToken, WrongTokenClass):
except (UnknownToken, WrongTokenClass, ToOld):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change is important to properly mark token with active: false on the introspection endpoint, when the token has expired.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Maybe this should be split into its own PR, along with the relevant test case.)

return {"response_args": _resp}

grant = _session_info["grant"]
Expand Down
67 changes: 47 additions & 20 deletions src/idpyoidc/server/oauth2/token_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ def _mint_token(
token_args = meth(_context, client_id, token_args)

if token_args:
_args = {"token_args": token_args}
_args = token_args
else:
_args = {}

Expand Down Expand Up @@ -258,7 +258,6 @@ def process_request(self, req: Union[Message, dict], **kwargs):
if (
issue_refresh
and "refresh_token" in _supports_minting
and "refresh_token" in grant_types_supported
):
try:
refresh_token = self._mint_token(
Expand Down Expand Up @@ -370,7 +369,7 @@ def process_request(self, req: Union[Message, dict], **kwargs):
token_type = "DPoP"

token = _grant.get_token(token_value)
scope = _grant.find_scope(token.based_on)
scope = _grant.find_scope(token)
if "scope" in req:
scope = req["scope"]
access_token = self._mint_token(
Expand Down Expand Up @@ -543,6 +542,27 @@ def post_parse_request(self, request, client_id="", **kwargs):
)

resp = self._enforce_policy(request, token, config)
if isinstance(resp, TokenErrorResponse):
return resp

scopes = resp.get("scope", [])
scopes = _context.scopes_handler.filter_scopes(scopes, client_id=resp["client_id"])

if not scopes:
logger.error("All requested scopes have been filtered out.")
return self.error_cls(
error="invalid_scope", error_description="Invalid requested scopes"
)

_requested_token_type = resp.get(
"requested_token_type", "urn:ietf:params:oauth:token-type:access_token"
)
_token_class = self.token_types_mapping[_requested_token_type]
if _token_class == "refresh_token" and "offline_access" not in scopes:
return TokenErrorResponse(
error="invalid_request",
error_description="Exchanging this subject token to refresh token forbidden",
)

return resp

Expand Down Expand Up @@ -572,7 +592,7 @@ def _enforce_policy(self, request, token, config):
error_description="Unsupported requested token type",
)

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

def token_exchange_response(self, token):
def token_exchange_response(self, token, issued_token_type):
response_args = {}
response_args["access_token"] = token.value
response_args["scope"] = token.scope
response_args["issued_token_type"] = token.token_class
response_args["issued_token_type"] = issued_token_type

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

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

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_grant=grant,
original_session_id=sid,
user_id=_session_info["user_id"],
client_id=request["client_id"],
token_usage_rules=_token_usage_rules,
scopes=scopes,
)

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

_token_args = None
if resources:
_token_args={"resources": resources}

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": resources},
scope=scopes,
token_args=_token_args,
token_type=_token_type,
)
new_token.expires_at = token.expires_at
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)
return self.token_exchange_response(new_token, _requested_token_type)

def _validate_configuration(self, config):
if "requested_token_types_supported" not in config:
Expand Down Expand Up @@ -759,18 +788,16 @@ def validate_token_exchange_policy(request, context, subject_token, **kwargs):
if "offline_access" not in subject_token.scope:
return TokenErrorResponse(
error="invalid_request",
error_description=f"Exchange {request['subject_token_type']} to refresh token "
f"forbidden",
error_description=f"Exchange {request['subject_token_type']} to refresh token forbidden",
)

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

return request
2 changes: 1 addition & 1 deletion src/idpyoidc/server/oidc/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -361,7 +361,7 @@ def parse_request(self, request, http_info=None, **kwargs):

# Verify that the client is allowed to do this
auth_info = self.client_authentication(request, http_info, **kwargs)
if not auth_info or auth_info["method"] == "none":
if not auth_info:
pass
elif isinstance(auth_info, ResponseMessage):
return auth_info
Expand Down
3 changes: 1 addition & 2 deletions src/idpyoidc/server/oidc/token_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,6 @@ def process_request(self, req: Union[Message, dict], **kwargs):
if (
issue_refresh
and "refresh_token" in _supports_minting
and "refresh_token" in grant_types_supported
):
try:
refresh_token = self._mint_token(
Expand Down Expand Up @@ -237,7 +236,7 @@ def process_request(self, req: Union[Message, dict], **kwargs):
token_type = "DPoP"

token = _grant.get_token(token_value)
scope = _grant.find_scope(token.based_on)
scope = _grant.find_scope(token)
if "scope" in req:
scope = req["scope"]
access_token = self._mint_token(
Expand Down
2 changes: 1 addition & 1 deletion src/idpyoidc/server/oidc/userinfo.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ def parse_request(self, request, http_info=None, **kwargs):
try:
auth_info = self.client_authentication(request, http_info, **kwargs)
except ClientAuthenticationError as e:
return self.error_cls(error="invalid_token", error_description=e.args[0])
return self.error_cls(error="invalid_token", error_description="Invalid token")

if isinstance(auth_info, ResponseMessage):
return auth_info
Expand Down
Loading