Skip to content
Merged
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
1 change: 1 addition & 0 deletions doc/intro.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ IdpyOIDC implements the following standards:
* `OpenID Connect Front-Channel Logout 1.0 <https://openid.net/specs/openid-connect-frontchannel-1_0.html>`_
* `OAuth2 Token introspection <https://tools.ietf.org/html/rfc7662>`_
* `OAuth2 Token exchange <https://datatracker.ietf.org/doc/html/rfc8693>`_
* `OAuth2 Resource Indicators <https://datatracker.ietf.org/doc/rfc8707/>`_
* `The OAuth 2.0 Authorization Framework: JWT-Secured Authorization Request (JAR) <https://datatracker.ietf.org/doc/html/rfc9101>`_

It also comes with the following `add_on` modules.
Expand Down
64 changes: 64 additions & 0 deletions doc/server/contents/conf.rst
Original file line number Diff line number Diff line change
Expand Up @@ -884,3 +884,67 @@ idpyoidc\.server\.configure module
:undoc-members:
:show-inheritance:


==============
Resource Indicators
==============
There are two possible ways to configure Resource Indicators in OIDC-OP, globally and per-client.
For the first case the configuration is passed in the Authorization or Access Token endpoint arguments throught the
`resource_indicators` dictionary.

If present, the resource indicators configuration should contain a `policy` dictionary
that defines the behaviour of the specific endpoint. The policy
is mapped to a dictionary with the keys `callable` (mandatory), which must be a
python callable or a string that represents the path to a python callable, and
`kwargs` (optional), which must be a dict of key-value arguments that will be
passed to the callable.

The resource indicators configuration may also contain a `resource_servers_per_client`
dictionary that defines a mapping between oidc-op registered clients with key the equivalent `client id` and resources to whom this client
is eligible to request access.

"resource_indicators":{
"policy": {
"callable": validate_authorization_resource_indicators_policy,
"kwargs": {
"resource_servers_per_client": {
"CLIENT_1": ["RESOURCE_1"],
"CLIENT_2": ["RESOURCE_1", "RESOURCE_2"]
},
},
},
},
}

For the per-client configuration a similar configuration scheme should be present in the client's
metadata under the `resource_indicators` key with slight difference. The `policy` mapping should be set a value for a
key `authorization_code` or `access_token` in order to indicate the endpoint that this resource indicators policy is reffered to.
In addition, the `resource_servers_per_client` value is a list of the permitted resources.

For example::

"resource_indicators":{
"authorization_code": {
"policy": {
"callable": validate_authorization_resource_indicators_policy,
"kwargs": {
"resource_servers_per_client": ["RESOURCE_1"],
},
},
},
},
}

The policy callable accepts a specific argument list and must return the altered token
request or raise an exception.

For example::

def validate_resource_indicators_policy(request, context, **kwargs):
if some_condition in request:
return TokenErrorResponse(
error="invalid_request", error_description="Some error occured"
)

return request

1 change: 1 addition & 0 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ pytest-isort>=1.3.0
pytest-localserver>=0.5.0
flake8
bandit
urllib3<1.27
101 changes: 98 additions & 3 deletions src/idpyoidc/server/oauth2/authorization.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from cryptojwt.utils import as_bytes
from cryptojwt.utils import b64e

from idpyoidc.exception import ImproperlyConfigured
from idpyoidc.exception import ParameterError
from idpyoidc.exception import URIError
from idpyoidc.message import Message
Expand All @@ -39,6 +40,7 @@
from idpyoidc.time_util import utc_time_sans_frac
from idpyoidc.util import rndstr
from idpyoidc.util import split_uri
from idpyoidc.util import importer

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -277,6 +279,53 @@ def check_unknown_scopes_policy(request_info, client_id, endpoint_context):
raise UnAuthorizedClientScope()


def validate_resource_indicators_policy(request, context, **kwargs):
if "resource" not in request:
return oauth2.AuthorizationErrorResponse(
error="invalid_target",
error_description="Missing resource parameter",
)

resource_servers_per_client = kwargs["resource_servers_per_client"]
client_id = request["client_id"]

if isinstance(resource_servers_per_client, dict) and client_id not in resource_servers_per_client:
return oauth2.AuthorizationErrorResponse(
error="invalid_target",
error_description=f"Resources for client {client_id} not found",
)

if isinstance(resource_servers_per_client, dict):
permitted_resources = [res for res in resource_servers_per_client[client_id]]
else:
permitted_resources = [res for res in resource_servers_per_client]

common_resources = list(set(request["resource"]).intersection(set(permitted_resources)))
if not common_resources:
return oauth2.AuthorizationErrorResponse(
error="invalid_target",
error_description=f"Invalid resource requested by client {client_id}",
)

common_resources = [r for r in common_resources if r in context.cdb.keys()]
if not common_resources:
return oauth2.AuthorizationErrorResponse(
error="invalid_target",
error_description=f"Invalid resource requested by client {client_id}",
)

if client_id not in common_resources:
common_resources.append(client_id)

request["resource"] = common_resources

permitted_scopes = [context.cdb[r]["allowed_scopes"] for r in common_resources]
permitted_scopes = [r for res in permitted_scopes for r in res]
scopes = list(set(request.get("scope", [])).intersection(set(permitted_scopes)))
request["scope"] = scopes
return request


class Authorization(Endpoint):
request_cls = oauth2.AuthorizationRequest
response_cls = oauth2.AuthorizationResponse
Expand Down Expand Up @@ -304,6 +353,8 @@ class Authorization(Endpoint):

def __init__(self, server_get, **kwargs):
Endpoint.__init__(self, server_get, **kwargs)

self.resource_indicators_config = kwargs.get("resource_indicators", None)
self.post_parse_request.append(self._do_request_uri)
self.post_parse_request.append(self._post_parse_request)
self.allowed_request_algorithms = AllowedAlgorithms(ALG_PARAMS)
Expand Down Expand Up @@ -461,8 +512,45 @@ def _post_parse_request(self, request, client_id, endpoint_context, **kwargs):
else:
request["redirect_uri"] = redirect_uri

if ("resource_indicators" in _cinfo
and "authorization_code" in _cinfo["resource_indicators"]):
resource_indicators_config = _cinfo["resource_indicators"]["authorization_code"]
else:
resource_indicators_config = self.resource_indicators_config

if resource_indicators_config is not None:
if "policy" not in resource_indicators_config:
policy = {"policy": {"callable": validate_resource_indicators_policy}}
resource_indicators_config.update(policy)
request = self._enforce_resource_indicators_policy(request, resource_indicators_config)

return request

def _enforce_resource_indicators_policy(self, request, config):
_context = self.server_get("endpoint_context")

policy = config["policy"]
callable = policy["callable"]
kwargs = policy.get("kwargs", {})

if kwargs.get("resource_servers_per_client", None) is None:
kwargs["resource_servers_per_client"] = {
request["client_id"]: request["client_id"]
}

if isinstance(callable, str):
try:
fn = importer(callable)
except Exception:
raise ImproperlyConfigured(f"Error importing {callable} policy callable")
else:
fn = callable
try:
return fn(request, context=_context, **kwargs)
except Exception as e:
logger.error(f"Error while executing the {fn} policy callable: {e}")
return self.error_cls(error="server_error", error_description="Internal server error")

def pick_authn_method(self, request, redirect_uri, acr=None, **kwargs):
_context = self.server_get("endpoint_context")
auth_id = kwargs.get("auth_method_id")
Expand Down Expand Up @@ -750,10 +838,17 @@ def create_authn_response(self, request: Union[dict, Message], sid: str) -> dict
_mngr = _context.session_manager
_sinfo = _mngr.get_session_info(sid, grant=True)

scope = []
resource_scopes = []
if request.get("scope"):
aresp["scope"] = _context.scopes_handler.filter_scopes(
request["scope"], _sinfo["client_id"]
)
scope = request.get("scope")
if request.get("resource"):
resource_scopes = [_context.cdb[s]["scope"] for s in request.get("resource") if s in _context.cdb.keys() and _context.cdb[s].get("scope")]
resource_scopes = [item for sublist in resource_scopes for item in sublist]

aresp["scope"] = _context.scopes_handler.filter_scopes(
list(set(scope+resource_scopes)), _sinfo["client_id"]
)

rtype = set(request["response_type"][:])
handled_response_type = []
Expand Down
102 changes: 101 additions & 1 deletion src/idpyoidc/server/oauth2/token_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,54 @@ def _mint_token(

return token

def validate_resource_indicators_policy(request, context, **kwargs):
if "resource" not in request:
return TokenErrorResponse(
error="invalid_target",
error_description="Missing resource parameter",
)

resource_servers_per_client = kwargs["resource_servers_per_client"]
client_id = request["client_id"]

resource_servers_per_client = kwargs.get("resource_servers_per_client", None)

if isinstance(resource_servers_per_client, dict) and client_id not in resource_servers_per_client:
return TokenErrorResponse(
error="invalid_target",
error_description=f"Resources for client {client_id} not found",
)

if isinstance(resource_servers_per_client, dict):
permitted_resources = [res for res in resource_servers_per_client[client_id]]
else:
permitted_resources = [res for res in resource_servers_per_client]

common_resources = list(set(request["resource"]).intersection(set(permitted_resources)))
if not common_resources:
return TokenErrorResponse(
error="invalid_target",
error_description=f"Invalid resource requested by client {client_id}",
)

common_resources = [r for r in common_resources if r in context.cdb.keys()]
if not common_resources:
return TokenErrorResponse(
error="invalid_target",
error_description=f"Invalid resource requested by client {client_id}",
)

if client_id not in common_resources:
common_resources.append(client_id)

request["resource"] = common_resources

permitted_scopes = [context.cdb[r]["allowed_scopes"] for r in common_resources]
permitted_scopes = [r for res in permitted_scopes for r in res]
scopes = list(set(request.get("scope", [])).intersection(set(permitted_scopes)))
request["scope"] = scopes
return request


class AccessTokenHelper(TokenEndpointHelper):
def process_request(self, req: Union[Message, dict], **kwargs):
Expand Down Expand Up @@ -132,6 +180,24 @@ def process_request(self, req: Union[Message, dict], **kwargs):
logger.warning("Client using token it was not given")
return self.error_cls(error="invalid_grant", error_description="Wrong client")

_cinfo = self.endpoint.server_get("endpoint_context").cdb.get(client_id)

if ("resource_indicators" in _cinfo
and "access_token" in _cinfo["resource_indicators"]):
resource_indicators_config = _cinfo["resource_indicators"]["access_token"]
else:
resource_indicators_config = self.endpoint.kwargs.get("resource_indicators", None)

if resource_indicators_config is not None:
if "policy" not in resource_indicators_config:
policy = {"policy": {"callable": validate_resource_indicators_policy}}
resource_indicators_config.update(policy)

req = self._enforce_resource_indicators_policy(req, resource_indicators_config)

if isinstance(req, TokenErrorResponse):
return req

if "grant_types_supported" in _context.cdb[client_id]:
grant_types_supported = _context.cdb[client_id].get("grant_types_supported")
else:
Expand All @@ -154,19 +220,33 @@ def process_request(self, req: Union[Message, dict], **kwargs):
logger.debug("All checks OK")

issue_refresh = kwargs.get("issue_refresh", False)

if resource_indicators_config is not None:
scope = req["scope"]
else:
scope = grant.scope

_response = {
"token_type": "Bearer",
"scope": grant.scope,
"scope": scope,
}

if "access_token" in _supports_minting:

resources = req.get("resource", None)
if resources:
token_args = {"resources": resources}
else:
token_args = None

try:
token = self._mint_token(
token_class="access_token",
grant=grant,
session_id=_session_info["branch_id"],
client_id=_session_info["client_id"],
based_on=_based_on,
token_args=token_args
)
except MintingNotAllowed as err:
logger.warning(err)
Expand Down Expand Up @@ -200,6 +280,26 @@ def process_request(self, req: Union[Message, dict], **kwargs):

return _response

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

policy = config["policy"]
callable = policy["callable"]
kwargs = policy.get("kwargs", {})

if isinstance(callable, str):
try:
fn = importer(callable)
except Exception:
raise ImproperlyConfigured(f"Error importing {callable} policy callable")
else:
fn = callable
try:
return fn(request, context=_context, **kwargs)
except Exception as e:
logger.error(f"Error while executing the {fn} policy callable: {e}")
return self.error_cls(error="server_error", error_description="Internal server error")

def post_parse_request(
self, request: Union[Message, dict], client_id: Optional[str] = "", **kwargs
):
Expand Down
Loading