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

Commit f091c20

Browse files
committed
Refactor scopes
1 parent 9a2f6cf commit f091c20

14 files changed

Lines changed: 484 additions & 187 deletions

docs/source/contents/conf.rst

Lines changed: 44 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,28 @@ sub_funcs
5353

5454
Optional. Functions involved in *sub*ject value creation.
5555
56+
57+
58+
scopes_mapping
59+
##############
60+
61+
A dict defining the scopes that are allowed to be used per client and the claims
62+
they map to (defaults to the scopes mapping described in the spec). If we want
63+
to define a scope that doesn't map to claims (e.g. offline_access) then we
64+
simply map it to an empty list. E.g.::
65+
{
66+
"scope_a": ["claim1", "claim2"],
67+
"scope_b": []
68+
}
69+
*Note*: For OIDC the `openid` scope must be present in this mapping.
70+
71+
72+
allowed_scopes
73+
##############
74+
75+
A list with the scopes that are allowed to be used (defaults to the keys in scopes_mapping).
76+
77+
5678
------
5779
add_on
5880
------
@@ -67,21 +89,6 @@ An example::
6789
"code_challenge_method": "S256 S384 S512"
6890
}
6991
},
70-
"claims": {
71-
"function": "oidcop.oidc.add_on.custom_scopes.add_custom_scopes",
72-
"kwargs": {
73-
"research_and_scholarship": [
74-
"name",
75-
"given_name",
76-
"family_name",
77-
"email",
78-
"email_verified",
79-
"sub",
80-
"iss",
81-
"eduperson_scoped_affiliation"
82-
]
83-
}
84-
}
8592
}
8693

8794
The provided add-ons can be seen in the following sections.
@@ -176,6 +183,8 @@ An example::
176183
backchannel_logout_supported: True
177184
backchannel_logout_session_supported: True
178185
check_session_iframe: https://127.0.0.1:5000/check_session_iframe
186+
scopes_supported: ["openid", "profile", "random"]
187+
claims_supported: ["sub", "given_name", "birthdate"]
179188

180189
---------
181190
client_db
@@ -720,3 +729,23 @@ grant_types_supported
720729
---------------------
721730

722731
Configure the allowed grant types on the token endpoint.
732+
733+
--------------
734+
scopes_mapping
735+
--------------
736+
737+
A dict defining the scopes that are allowed to be used per client and the claims
738+
they map to (defaults to the scopes mapping described in the spec). If we want
739+
to define a scope that doesn't map to claims (e.g. offline_access) then we
740+
simply map it to an empty list. E.g.::
741+
{
742+
"scope_a": ["claim1", "claim2"],
743+
"scope_b": []
744+
}
745+
746+
--------------
747+
allowed_scopes
748+
--------------
749+
750+
A list with the scopes that are allowed to be used (defaults to the keys in the
751+
clients scopes_mapping).

src/oidcop/authz/__init__.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,10 @@ def __call__(
8080
grant.resources = resources
8181

8282
# After this is where user consent should be handled
83-
scopes = request.get("scope", [])
84-
grant.scope = scopes
83+
scopes = grant.scope
84+
if not scopes:
85+
scopes = request.get("scope", [])
86+
grant.scope = scopes
8587
grant.claims = self.server_get("endpoint_context").claims_interface.get_claims_all_usage(
8688
session_id=session_id, scopes=scopes
8789
)

src/oidcop/configure.py

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from typing import Union
1111

1212
from oidcop.logging import configure_logging
13+
from oidcop.scopes import SCOPE2CLAIMS
1314
from oidcop.utils import load_yaml_config
1415

1516
DEFAULT_FILE_ATTRIBUTE_NAMES = [
@@ -78,6 +79,7 @@
7879
"refresh": {"class": "oidcop.token.jwt_token.JWTToken", "kwargs": {"lifetime": 86400}, },
7980
"id_token": {"class": "oidcop.token.id_token.IDToken", "kwargs": {}},
8081
},
82+
"scopes_mapping": SCOPE2CLAIMS,
8183
}
8284

8385
AS_DEFAULT_CONFIG = copy.deepcopy(OP_DEFAULT_CONFIG)
@@ -274,12 +276,39 @@ class OPConfiguration(EntityConfiguration):
274276
"Provider configuration"
275277
default_config = OP_DEFAULT_CONFIG
276278
parameter = EntityConfiguration.parameter.copy()
277-
parameter.update({
278-
"id_token": None,
279-
"login_hint2acrs": {},
280-
"login_hint_lookup": None,
281-
"sub_func": {}
282-
})
279+
parameter.update(
280+
{
281+
"id_token": None,
282+
"login_hint2acrs": {},
283+
"login_hint_lookup": None,
284+
"sub_func": {},
285+
"scopes_mapping": {},
286+
"scopes_supported": None,
287+
"advertised_scopes": None,
288+
}
289+
)
290+
291+
def __init__(
292+
self,
293+
conf: Dict,
294+
base_path: Optional[str] = "",
295+
entity_conf: Optional[List[dict]] = None,
296+
domain: Optional[str] = "",
297+
port: Optional[int] = 0,
298+
file_attributes: Optional[List[str]] = None,
299+
):
300+
super().__init__(
301+
conf=conf,
302+
base_path=base_path,
303+
entity_conf=entity_conf,
304+
domain=domain,
305+
port=port,
306+
file_attributes=file_attributes,
307+
)
308+
scopes_mapping = self.scopes_mapping
309+
if "advertised_scopes" not in self:
310+
self["advertised_scopes"] = list(scopes_mapping.keys())
311+
283312

284313
class ASConfiguration(EntityConfiguration):
285314
"Authorization server configuration"

src/oidcop/endpoint_context.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import json
22
import logging
33
from typing import Any
4+
from typing import Callable
45
from typing import Optional
56
from typing import Union
67

@@ -121,13 +122,15 @@ class EndpointContext(OidcContext):
121122
def __init__(
122123
self,
123124
conf: Union[dict, OPConfiguration],
125+
server_get: Callable,
124126
keyjar: Optional[KeyJar] = None,
125127
cwd: Optional[str] = "",
126128
cookie_handler: Optional[Any] = None,
127129
httpc: Optional[Any] = None,
128130
):
129131
OidcContext.__init__(self, conf, keyjar, entity_id=conf.get("issuer", ""))
130132
self.conf = conf
133+
self.server_get = server_get
131134

132135
_client_db = conf.get("client_db")
133136
if _client_db:
@@ -248,10 +251,14 @@ def set_scopes_handler(self):
248251
_spec = self.conf.get("scopes_handler")
249252
if _spec:
250253
_kwargs = _spec.get("kwargs", {})
251-
_cls = importer(_spec["class"])(**_kwargs)
252-
self.scopes_handler = _cls(_kwargs)
254+
_cls = importer(_spec["class"])
255+
self.scopes_handler = _cls(self.server_get, **_kwargs)
253256
else:
254-
self.scopes_handler = Scopes()
257+
self.scopes_handler = Scopes(
258+
self.server_get,
259+
allowed_scopes=self.conf.get("allowed_scopes"),
260+
scopes_mapping=self.conf.get("scopes_mapping"),
261+
)
255262

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

327334
if "scopes_supported" not in _provider_info:
328-
_provider_info["scopes_supported"] = [s for s in self.scope2claims.keys()]
335+
_provider_info["scopes_supported"] = self.scopes_handler.get_allowed_scopes()
329336
if "claims_supported" not in _provider_info:
330-
_provider_info["claims_supported"] = STANDARD_CLAIMS[:]
337+
_provider_info["claims_supported"] = list(
338+
self.scopes_handler.scopes_to_claims(_provider_info["scopes_supported"]).keys()
339+
)
331340

332341
return _provider_info

src/oidcop/oauth2/authorization.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -244,15 +244,17 @@ def authn_args_gather(
244244
return authn_args
245245

246246

247-
def check_unknown_scopes_policy(request_info, cinfo, endpoint_context):
248-
op_capabilities = endpoint_context.conf["capabilities"]
249-
client_allowed_scopes = cinfo.get("allowed_scopes") or op_capabilities["scopes_supported"]
247+
def check_unknown_scopes_policy(request_info, client_id, endpoint_context):
248+
if not endpoint_context.conf["capabilities"].get("deny_unknown_scopes"):
249+
return
250+
251+
allowed_scopes = endpoint_context.scopes_handler.get_allowed_scopes(client_id=client_id)
250252

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

258260

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

683685
if request.get("scope"):
684-
aresp["scope"] = request["scope"]
686+
aresp["scope"] = _context.scopes_handler.filter_scopes(
687+
request["scope"], _sinfo["client_id"]
688+
)
685689

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

905909
# this apply the default optionally deny_unknown_scopes policy
906-
if cinfo:
907-
check_unknown_scopes_policy(request, cinfo, _context)
910+
check_unknown_scopes_policy(request, _cid, _context)
908911

909912
if http_info is None:
910913
http_info = {}

src/oidcop/oidc/add_on/custom_scopes.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,22 @@ def add_custom_scopes(endpoint, **kwargs):
1010
:param endpoint: A dictionary with endpoint instances as values
1111
"""
1212
# Just need an endpoint, anyone will do
13+
LOGGER.warning(
14+
"The custom_scopes add on is deprecated. The `scopes_mapping` config "
15+
"option should be used instead."
16+
)
1317
_endpoint = list(endpoint.values())[0]
1418

1519
_scopes2claims = SCOPE2CLAIMS.copy()
1620
_scopes2claims.update(kwargs)
1721
_context = _endpoint.server_get("endpoint_context")
18-
_context.scope2claims = _scopes2claims
22+
_context.scopes_handler.scopes_mapping = _scopes2claims
1923

2024
pi = _context.provider_info
2125
_scopes = set(pi.get("scopes_supported", []))
2226
_scopes.update(set(kwargs.keys()))
2327
pi["scopes_supported"] = list(_scopes)
28+
_context.scopes_handler.allowed_scopes = pi["scopes_supported"]
2429

2530
_claims = set(pi.get("claims_supported", []))
2631
for vals in kwargs.values():

src/oidcop/scopes.py

Lines changed: 44 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,6 @@
2525
}
2626

2727

28-
def available_scopes(endpoint_context):
29-
_supported = endpoint_context.provider_info.get("scopes_supported")
30-
if _supported:
31-
return [s for s in endpoint_context.scope2claims.keys() if s in _supported]
32-
else:
33-
return [s for s in endpoint_context.scope2claims.keys()]
34-
35-
3628
def convert_scopes2claims(scopes, allowed_claims=None, scope2claim_map=None):
3729
scope2claim_map = scope2claim_map or SCOPE2CLAIMS
3830

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

5446

5547
class Scopes:
56-
def __init__(self):
57-
pass
48+
def __init__(self, server_get, allowed_scopes=None, scopes_mapping=None):
49+
self.server_get = server_get
50+
if not scopes_mapping:
51+
scopes_mapping = dict(SCOPE2CLAIMS)
52+
self.scopes_mapping = scopes_mapping
53+
if not allowed_scopes:
54+
allowed_scopes = list(scopes_mapping.keys())
55+
self.allowed_scopes = allowed_scopes
5856

59-
def allowed_scopes(self, client_id, endpoint_context):
57+
def get_allowed_scopes(self, client_id=None):
6058
"""
6159
Returns the set of scopes that a specific client can use.
6260
6361
:param client_id: The client identifier
64-
:param endpoint_context: A EndpointContext instance
6562
:returns: List of scope names. Can be empty.
6663
"""
67-
_cli = endpoint_context.cdb.get(client_id)
68-
if _cli is not None:
69-
_scopes = _cli.get("allowed_scopes")
70-
if _scopes:
71-
return _scopes
72-
else:
73-
return available_scopes(endpoint_context)
74-
return []
75-
76-
def filter_scopes(self, client_id, endpoint_context, scopes):
77-
allowed_scopes = self.allowed_scopes(client_id, endpoint_context)
64+
allowed_scopes = self.allowed_scopes
65+
if client_id:
66+
client = self.server_get("endpoint_context").cdb.get(client_id)
67+
if client is not None:
68+
if "allowed_scopes" in client:
69+
allowed_scopes = client.get("allowed_scopes")
70+
elif "scopes_mapping" in client:
71+
allowed_scopes = list(client.get("scopes_mapping").keys())
72+
73+
return allowed_scopes
74+
75+
def get_scopes_mapping(self, client_id=None):
76+
"""
77+
Returns the mapping of scopes to claims fora specific client.
78+
79+
:param client_id: The client identifier
80+
:returns: Dict of scopes to claims. Can be empty.
81+
"""
82+
scopes_mapping = self.scopes_mapping
83+
if client_id:
84+
client = self.server_get("endpoint_context").cdb.get(client_id)
85+
if client is not None:
86+
scopes_mapping = client.get("scopes_mapping", scopes_mapping)
87+
return scopes_mapping
88+
89+
def filter_scopes(self, scopes, client_id=None):
90+
allowed_scopes = self.get_allowed_scopes(client_id)
7891
return [s for s in scopes if s in allowed_scopes]
92+
93+
def scopes_to_claims(self, scopes, scopes_mapping=None, client_id=None):
94+
if not scopes_mapping:
95+
scopes_mapping = self.get_scopes_mapping(client_id)
96+
97+
scopes = self.filter_scopes(scopes, client_id)
98+
99+
return convert_scopes2claims(scopes, scope2claim_map=scopes_mapping)

src/oidcop/server.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,12 @@ def __init__(
6767
ImpExp.__init__(self)
6868
self.conf = conf
6969
self.endpoint_context = EndpointContext(
70-
conf=conf, keyjar=keyjar, cwd=cwd, cookie_handler=cookie_handler, httpc=httpc,
70+
conf=conf,
71+
server_get=self.server_get,
72+
keyjar=keyjar,
73+
cwd=cwd,
74+
cookie_handler=cookie_handler,
75+
httpc=httpc,
7176
)
7277
self.endpoint_context.authz = self.do_authz()
7378

0 commit comments

Comments
 (0)