Skip to content
This repository was archived by the owner on Jun 23, 2023. It is now read-only.
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
64 changes: 49 additions & 15 deletions docs/source/contents/conf.rst
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,33 @@ sub_funcs

Optional. Functions involved in *sub*ject value creation.


scopes_mapping
##############

A dict defining the scopes that are allowed to be used per client and the claims
they map to (defaults to the scopes mapping described in the spec). If we want
to define a scope that doesn't map to claims (e.g. offline_access) then we
simply map it to an empty list. E.g.::
{
"scope_a": ["claim1", "claim2"],
"scope_b": []
}
*Note*: For OIDC the `openid` scope must be present in this mapping.


allowed_scopes
##############

A list with the scopes that are allowed to be used (defaults to the keys in scopes_mapping).


advertised_scopes
#################

A list with the scopes that will be advertised in the well-known endpoint (defaults to allowed_scopes).


------
add_on
------
Expand All @@ -67,21 +94,6 @@ An example::
"code_challenge_method": "S256 S384 S512"
}
},
"claims": {
"function": "oidcop.oidc.add_on.custom_scopes.add_custom_scopes",
"kwargs": {
"research_and_scholarship": [
"name",
"given_name",
"family_name",
"email",
"email_verified",
"sub",
"iss",
"eduperson_scoped_affiliation"
]
}
}
}

The provided add-ons can be seen in the following sections.
Expand Down Expand Up @@ -176,6 +188,8 @@ An example::
backchannel_logout_supported: True
backchannel_logout_session_supported: True
check_session_iframe: https://127.0.0.1:5000/check_session_iframe
scopes_supported: ["openid", "profile", "random"]
claims_supported: ["sub", "given_name", "birthdate"]

---------
client_db
Expand Down Expand Up @@ -720,3 +734,23 @@ grant_types_supported
---------------------

Configure the allowed grant types on the token endpoint.

--------------
scopes_mapping
--------------

A dict defining the scopes that are allowed to be used per client and the claims
they map to (defaults to the scopes mapping described in the spec). If we want
to define a scope that doesn't map to claims (e.g. offline_access) then we
simply map it to an empty list. E.g.::
{
"scope_a": ["claim1", "claim2"],
"scope_b": []
}

--------------
allowed_scopes
--------------

A list with the scopes that are allowed to be used (defaults to the keys in the
clients scopes_mapping).
6 changes: 4 additions & 2 deletions src/oidcop/authz/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,10 @@ def __call__(
grant.resources = resources

# After this is where user consent should be handled
scopes = request.get("scope", [])
grant.scope = scopes
scopes = grant.scope
if not scopes:
scopes = request.get("scope", [])
grant.scope = scopes
grant.claims = self.server_get("endpoint_context").claims_interface.get_claims_all_usage(
session_id=session_id, scopes=scopes
)
Expand Down
41 changes: 35 additions & 6 deletions src/oidcop/configure.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from typing import Union

from oidcop.logging import configure_logging
from oidcop.scopes import SCOPE2CLAIMS
from oidcop.utils import load_yaml_config

DEFAULT_FILE_ATTRIBUTE_NAMES = [
Expand Down Expand Up @@ -78,6 +79,7 @@
"refresh": {"class": "oidcop.token.jwt_token.JWTToken", "kwargs": {"lifetime": 86400}, },
"id_token": {"class": "oidcop.token.id_token.IDToken", "kwargs": {}},
},
"scopes_mapping": SCOPE2CLAIMS,
}

AS_DEFAULT_CONFIG = copy.deepcopy(OP_DEFAULT_CONFIG)
Expand Down Expand Up @@ -274,12 +276,39 @@ class OPConfiguration(EntityConfiguration):
"Provider configuration"
default_config = OP_DEFAULT_CONFIG
parameter = EntityConfiguration.parameter.copy()
parameter.update({
"id_token": None,
"login_hint2acrs": {},
"login_hint_lookup": None,
"sub_func": {}
})
parameter.update(
{
"id_token": None,
"login_hint2acrs": {},
"login_hint_lookup": None,
"sub_func": {},
"scopes_mapping": {},
"scopes_supported": None,
"advertised_scopes": None,
}
)

def __init__(
self,
conf: Dict,
base_path: Optional[str] = "",
entity_conf: Optional[List[dict]] = None,
domain: Optional[str] = "",
port: Optional[int] = 0,
file_attributes: Optional[List[str]] = None,
):
super().__init__(
conf=conf,
base_path=base_path,
entity_conf=entity_conf,
domain=domain,
port=port,
file_attributes=file_attributes,
)
scopes_mapping = self.scopes_mapping
if "advertised_scopes" not in self:
self["advertised_scopes"] = list(scopes_mapping.keys())


class ASConfiguration(EntityConfiguration):
"Authorization server configuration"
Expand Down
19 changes: 14 additions & 5 deletions src/oidcop/endpoint_context.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import json
import logging
from typing import Any
from typing import Callable
from typing import Optional
from typing import Union

Expand Down Expand Up @@ -121,13 +122,15 @@ class EndpointContext(OidcContext):
def __init__(
self,
conf: Union[dict, OPConfiguration],
server_get: Callable,
keyjar: Optional[KeyJar] = None,
cwd: Optional[str] = "",
cookie_handler: Optional[Any] = None,
httpc: Optional[Any] = None,
):
OidcContext.__init__(self, conf, keyjar, entity_id=conf.get("issuer", ""))
self.conf = conf
self.server_get = server_get

_client_db = conf.get("client_db")
if _client_db:
Expand Down Expand Up @@ -248,10 +251,14 @@ def set_scopes_handler(self):
_spec = self.conf.get("scopes_handler")
if _spec:
_kwargs = _spec.get("kwargs", {})
_cls = importer(_spec["class"])(**_kwargs)
self.scopes_handler = _cls(_kwargs)
_cls = importer(_spec["class"])
self.scopes_handler = _cls(self.server_get, **_kwargs)
else:
self.scopes_handler = Scopes()
self.scopes_handler = Scopes(
self.server_get,
allowed_scopes=self.conf.get("allowed_scopes"),
scopes_mapping=self.conf.get("scopes_mapping"),
)

def do_add_on(self, endpoints):
_add_on_conf = self.conf.get("add_on")
Expand Down Expand Up @@ -325,8 +332,10 @@ def create_providerinfo(self, capabilities):
_provider_info["jwks_uri"] = self.jwks_uri

if "scopes_supported" not in _provider_info:
_provider_info["scopes_supported"] = [s for s in self.scope2claims.keys()]
_provider_info["scopes_supported"] = self.scopes_handler.get_allowed_scopes()
if "claims_supported" not in _provider_info:
_provider_info["claims_supported"] = STANDARD_CLAIMS[:]
_provider_info["claims_supported"] = list(
self.scopes_handler.scopes_to_claims(_provider_info["scopes_supported"]).keys()
)

return _provider_info
19 changes: 11 additions & 8 deletions src/oidcop/oauth2/authorization.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,15 +244,17 @@ def authn_args_gather(
return authn_args


def check_unknown_scopes_policy(request_info, cinfo, endpoint_context):
op_capabilities = endpoint_context.conf["capabilities"]
client_allowed_scopes = cinfo.get("allowed_scopes") or op_capabilities["scopes_supported"]
def check_unknown_scopes_policy(request_info, client_id, endpoint_context):
if not endpoint_context.conf["capabilities"].get("deny_unknown_scopes"):
return

allowed_scopes = endpoint_context.scopes_handler.get_allowed_scopes(client_id=client_id)

# this prevents that authz would be released for unavailable scopes
for scope in request_info["scope"]:
if op_capabilities.get("deny_unknown_scopes") and scope not in client_allowed_scopes:
if scope not in allowed_scopes:
_msg = "{} requested an unauthorized scope ({})"
logger.warning(_msg.format(cinfo["client_id"], scope))
logger.warning(_msg.format(client_id, scope))
raise UnAuthorizedClientScope()


Expand Down Expand Up @@ -681,7 +683,9 @@ def create_authn_response(self, request: Union[dict, Message], sid: str) -> dict
_sinfo = _mngr.get_session_info(sid, grant=True)

if request.get("scope"):
aresp["scope"] = request["scope"]
aresp["scope"] = _context.scopes_handler.filter_scopes(
request["scope"], _sinfo["client_id"]
)

rtype = set(request["response_type"][:])
handled_response_type = []
Expand Down Expand Up @@ -903,8 +907,7 @@ def process_request(
# logger.debug("client {}: {}".format(_cid, cinfo))

# this apply the default optionally deny_unknown_scopes policy
if cinfo:
check_unknown_scopes_policy(request, cinfo, _context)
check_unknown_scopes_policy(request, _cid, _context)

if http_info is None:
http_info = {}
Expand Down
7 changes: 6 additions & 1 deletion src/oidcop/oidc/add_on/custom_scopes.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,22 @@ def add_custom_scopes(endpoint, **kwargs):
:param endpoint: A dictionary with endpoint instances as values
"""
# Just need an endpoint, anyone will do
LOGGER.warning(
"The custom_scopes add on is deprecated. The `scopes_mapping` config "
"option should be used instead."
)
_endpoint = list(endpoint.values())[0]

_scopes2claims = SCOPE2CLAIMS.copy()
_scopes2claims.update(kwargs)
_context = _endpoint.server_get("endpoint_context")
_context.scope2claims = _scopes2claims
_context.scopes_handler.scopes_mapping = _scopes2claims

pi = _context.provider_info
_scopes = set(pi.get("scopes_supported", []))
_scopes.update(set(kwargs.keys()))
pi["scopes_supported"] = list(_scopes)
_context.scopes_handler.allowed_scopes = pi["scopes_supported"]

_claims = set(pi.get("claims_supported", []))
for vals in kwargs.values():
Expand Down
67 changes: 44 additions & 23 deletions src/oidcop/scopes.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,6 @@
}


def available_scopes(endpoint_context):
_supported = endpoint_context.provider_info.get("scopes_supported")
if _supported:
return [s for s in endpoint_context.scope2claims.keys() if s in _supported]
else:
return [s for s in endpoint_context.scope2claims.keys()]


def convert_scopes2claims(scopes, allowed_claims=None, scope2claim_map=None):
scope2claim_map = scope2claim_map or SCOPE2CLAIMS

Expand All @@ -53,26 +45,55 @@ def convert_scopes2claims(scopes, allowed_claims=None, scope2claim_map=None):


class Scopes:
def __init__(self):
pass
def __init__(self, server_get, allowed_scopes=None, scopes_mapping=None):
self.server_get = server_get
if not scopes_mapping:
scopes_mapping = dict(SCOPE2CLAIMS)
self.scopes_mapping = scopes_mapping
if not allowed_scopes:
allowed_scopes = list(scopes_mapping.keys())
self.allowed_scopes = allowed_scopes

def allowed_scopes(self, client_id, endpoint_context):
def get_allowed_scopes(self, client_id=None):
"""
Returns the set of scopes that a specific client can use.

:param client_id: The client identifier
:param endpoint_context: A EndpointContext instance
:returns: List of scope names. Can be empty.
"""
_cli = endpoint_context.cdb.get(client_id)
if _cli is not None:
_scopes = _cli.get("allowed_scopes")
if _scopes:
return _scopes
else:
return available_scopes(endpoint_context)
return []

def filter_scopes(self, client_id, endpoint_context, scopes):
allowed_scopes = self.allowed_scopes(client_id, endpoint_context)
allowed_scopes = self.allowed_scopes
if client_id:
client = self.server_get("endpoint_context").cdb.get(client_id)
if client is not None:
if "allowed_scopes" in client:
allowed_scopes = client.get("allowed_scopes")
elif "scopes_mapping" in client:
allowed_scopes = list(client.get("scopes_mapping").keys())

return allowed_scopes

def get_scopes_mapping(self, client_id=None):
"""
Returns the mapping of scopes to claims fora specific client.

:param client_id: The client identifier
:returns: Dict of scopes to claims. Can be empty.
"""
scopes_mapping = self.scopes_mapping
if client_id:
client = self.server_get("endpoint_context").cdb.get(client_id)
if client is not None:
scopes_mapping = client.get("scopes_mapping", scopes_mapping)
return scopes_mapping

def filter_scopes(self, scopes, client_id=None):
allowed_scopes = self.get_allowed_scopes(client_id)
return [s for s in scopes if s in allowed_scopes]

def scopes_to_claims(self, scopes, scopes_mapping=None, client_id=None):
if not scopes_mapping:
scopes_mapping = self.get_scopes_mapping(client_id)

scopes = self.filter_scopes(scopes, client_id)

return convert_scopes2claims(scopes, scope2claim_map=scopes_mapping)
7 changes: 6 additions & 1 deletion src/oidcop/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,12 @@ def __init__(
ImpExp.__init__(self)
self.conf = conf
self.endpoint_context = EndpointContext(
conf=conf, keyjar=keyjar, cwd=cwd, cookie_handler=cookie_handler, httpc=httpc,
conf=conf,
server_get=self.server_get,
keyjar=keyjar,
cwd=cwd,
cookie_handler=cookie_handler,
httpc=httpc,
)
self.endpoint_context.authz = self.do_authz()

Expand Down
Loading