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

Commit 547e45b

Browse files
committed
Support for claims_supported in provider config and make it matter.
1 parent 4a761b1 commit 547e45b

File tree

7 files changed

+199
-17
lines changed

7 files changed

+199
-17
lines changed

src/oidcendpoint/common/authorization.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@
3030
</html>"""
3131

3232
DEFAULT_SCOPES = list(SCOPE2CLAIMS.keys())
33+
_CLAIMS = set()
34+
for scope, claims in SCOPE2CLAIMS.items():
35+
_CLAIMS.update(set(claims))
36+
DEFAULT_CLAIMS = list(_CLAIMS)
3337

3438

3539
def inputs(form_args):

src/oidcendpoint/oidc/add_on/custom_scopes.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,9 @@ def add_custom_scopes(endpoint, **kwargs):
2424
if authz_enp:
2525
_supported = set(authz_enp.default_capabilities['scopes_supported'])
2626
_supported.update(set(kwargs.keys()))
27-
authz_enp.default_capabilities['scopes_supported'] = list(_supported)
27+
authz_enp.default_capabilities['scopes_supported'] = list(_supported)
28+
29+
_claims = set(authz_enp.default_capabilities['claims_supported'])
30+
for vals in kwargs.values():
31+
_claims.update(set(vals))
32+
authz_enp.default_capabilities['claims_supported'] = list(_claims)

src/oidcendpoint/oidc/authorization.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from oidcendpoint import sanitize
2121
from oidcendpoint.authn_event import create_authn_event
2222
from oidcendpoint.common.authorization import AllowedAlgorithms
23+
from oidcendpoint.common.authorization import DEFAULT_CLAIMS
2324
from oidcendpoint.common.authorization import DEFAULT_SCOPES
2425
from oidcendpoint.common.authorization import FORM_POST
2526
from oidcendpoint.common.authorization import authn_args_gather
@@ -198,6 +199,8 @@ class Authorization(Endpoint):
198199
"request_object_encryption_enc_values_supported": None,
199200
"grant_types_supported": ["authorization_code", "implicit"],
200201
"scopes_supported": DEFAULT_SCOPES,
202+
"claims_supported": DEFAULT_CLAIMS,
203+
"claim_types_supported": ["normal", "aggregated", "distributed"]
201204
}
202205

203206
def __init__(self, endpoint_context, **kwargs):

src/oidcendpoint/userinfo.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,18 @@
99
logger = logging.getLogger(__name__)
1010

1111

12-
def id_token_claims(session):
12+
def id_token_claims(session, provider_info):
1313
"""
1414
Pick the IdToken claims from the request
1515
1616
:param session: Session information
1717
:return: The IdToken claims
1818
"""
19-
itc = update_claims(session, "id_token", {})
19+
itc = update_claims(session, "id_token", provider_info=provider_info, old_claims={})
2020
return itc
2121

2222

23-
def update_claims(session, about, old_claims=None):
23+
def update_claims(session, about, provider_info, old_claims=None):
2424
"""
2525
2626
:param session:
@@ -45,6 +45,11 @@ def update_claims(session, about, old_claims=None):
4545
pass
4646
else:
4747
if _claims:
48+
# Deal only with supported claims
49+
_unsup = [c for c in _claims.keys() if c not in provider_info["claims_supported"]]
50+
for _c in _unsup:
51+
del _claims[_c]
52+
4853
# update with old claims, do not overwrite
4954
for key, val in old_claims.items():
5055
if key not in _claims:
@@ -130,7 +135,9 @@ def collect_user_info(
130135
if perm_set:
131136
uic = {key: uic[key] for key in uic if key in perm_set}
132137

133-
uic = update_claims(session, "userinfo", uic)
138+
uic = update_claims(session, "userinfo",
139+
provider_info=endpoint_context.provider_info,
140+
old_claims=uic)
134141

135142
if uic:
136143
userinfo_claims = Claims(**uic)
@@ -176,7 +183,7 @@ def userinfo_in_id_token_claims(endpoint_context, session, def_itc=None):
176183
else:
177184
itc = {}
178185

179-
itc.update(id_token_claims(session))
186+
itc.update(id_token_claims(session, provider_info=endpoint_context.provider_info))
180187

181188
if not itc:
182189
return None

tests/test_07_userinfo.py

Lines changed: 159 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
import os
33

44
import pytest
5+
from oidcmsg.message import Message
6+
from oidcmsg.oidc import OpenIDRequest
7+
from oidcmsg.oidc import OpenIDSchema
58

69
from oidcendpoint.authn_event import create_authn_event
710
from oidcendpoint.endpoint_context import EndpointContext
@@ -16,9 +19,6 @@
1619
from oidcendpoint.userinfo import claims_match
1720
from oidcendpoint.userinfo import collect_user_info
1821
from oidcendpoint.userinfo import update_claims
19-
from oidcmsg.message import Message
20-
from oidcmsg.oidc import OpenIDRequest
21-
from oidcmsg.oidc import OpenIDSchema
2222

2323
CLAIMS = {
2424
"userinfo": {
@@ -35,6 +35,15 @@
3535
},
3636
}
3737

38+
CLAIMS_2 = {
39+
"userinfo": {
40+
"eduperson_scoped_affiliation": {"essential": True},
41+
"nickname": None,
42+
"email": {"essential": True},
43+
"email_verified": {"essential": True},
44+
}
45+
}
46+
3847
OIDR = OpenIDRequest(
3948
response_type="code",
4049
client_id="client1",
@@ -138,16 +147,25 @@ def test_custom_scopes():
138147
"eduperson_scoped_affiliation",
139148
}
140149

150+
PROVIDER_INFO = {
151+
"claims_supported": ["auth_time", "acr", "given_name",
152+
"nickname",
153+
"email",
154+
"email_verified",
155+
"picture",
156+
"http://example.info/claims/groups",
157+
]
158+
}
141159

142160
def test_update_claims_authn_req_id_token():
143161
_session_info = {"authn_req": OIDR}
144-
claims = update_claims(_session_info, "id_token")
162+
claims = update_claims(_session_info, "id_token", PROVIDER_INFO)
145163
assert set(claims.keys()) == {"auth_time", "acr"}
146164

147165

148166
def test_update_claims_authn_req_userinfo():
149167
_session_info = {"authn_req": OIDR}
150-
claims = update_claims(_session_info, "userinfo")
168+
claims = update_claims(_session_info, "userinfo", PROVIDER_INFO)
151169
assert set(claims.keys()) == {
152170
"given_name",
153171
"nickname",
@@ -160,13 +178,13 @@ def test_update_claims_authn_req_userinfo():
160178

161179
def test_update_claims_authzreq_id_token():
162180
_session_info = {"authn_req": OIDR}
163-
claims = update_claims(_session_info, "id_token")
181+
claims = update_claims(_session_info, "id_token", PROVIDER_INFO)
164182
assert set(claims.keys()) == {"auth_time", "acr"}
165183

166184

167185
def test_update_claims_authzreq_userinfo():
168186
_session_info = {"authn_req": OIDR}
169-
claims = update_claims(_session_info, "userinfo")
187+
claims = update_claims(_session_info, "userinfo", PROVIDER_INFO)
170188
assert set(claims.keys()) == {
171189
"given_name",
172190
"nickname",
@@ -262,7 +280,10 @@ def create_endpoint_context(self):
262280
)
263281

264282
def test_collect_user_info(self):
265-
_session_info = {"authn_req": OIDR}
283+
_req = OIDR.copy()
284+
_req["claims"] = CLAIMS_2
285+
286+
_session_info = {"authn_req": _req}
266287
session = _session_info.copy()
267288
session["sub"] = "doe"
268289
session["uid"] = "diana"
@@ -271,7 +292,6 @@ def test_collect_user_info(self):
271292
res = collect_user_info(self.endpoint_context, session)
272293

273294
assert res == {
274-
"given_name": "Diana",
275295
"nickname": "Dina",
276296
"sub": "doe",
277297
"email": "diana@example.org",
@@ -324,3 +344,133 @@ def test_collect_user_info_scope_not_supported(self):
324344
}
325345

326346

347+
class TestCollectUserInfoCustomScopes:
348+
@pytest.fixture(autouse=True)
349+
def create_endpoint_context(self):
350+
self.endpoint_context = EndpointContext(
351+
{
352+
"userinfo": {"class": UserInfo, "kwargs": {"db": USERINFO_DB}},
353+
"password": "we didn't start the fire",
354+
"issuer": "https://example.com/op",
355+
"token_expires_in": 900,
356+
"grant_expires_in": 600,
357+
"refresh_token_expires_in": 86400,
358+
"endpoint": {
359+
"provider_config": {
360+
"path": "{}/.well-known/openid-configuration",
361+
"class": ProviderConfiguration,
362+
"kwargs": {},
363+
},
364+
"registration": {
365+
"path": "{}/registration",
366+
"class": Registration,
367+
"kwargs": {},
368+
},
369+
"authorization": {
370+
"path": "{}/authorization",
371+
"class": Authorization,
372+
"kwargs": {
373+
"response_types_supported": [
374+
" ".join(x) for x in RESPONSE_TYPES_SUPPORTED
375+
],
376+
"response_modes_supported": ["query", "fragment", "form_post"],
377+
"claims_parameter_supported": True,
378+
"request_parameter_supported": True,
379+
"request_uri_parameter_supported": True,
380+
},
381+
},
382+
},
383+
"add_on": {
384+
"custom_scopes": {
385+
"function": "oidcendpoint.oidc.add_on.custom_scopes.add_custom_scopes",
386+
"kwargs": {
387+
"research_and_scholarship": [
388+
"name",
389+
"given_name",
390+
"family_name",
391+
"email",
392+
"email_verified",
393+
"sub",
394+
"iss",
395+
"eduperson_scoped_affiliation",
396+
]
397+
},
398+
}
399+
},
400+
"jwks": {
401+
"public_path": "jwks.json",
402+
"key_defs": KEYDEFS,
403+
"uri_path": "static/jwks.json",
404+
},
405+
"authentication": {
406+
"anon": {
407+
"acr": INTERNETPROTOCOLPASSWORD,
408+
"class": "oidcendpoint.user_authn.user.NoAuthn",
409+
"kwargs": {"user": "diana"},
410+
}
411+
},
412+
"template_dir": "template",
413+
}
414+
)
415+
416+
def test_collect_user_info(self):
417+
_session_info = {"authn_req": OIDR}
418+
session = _session_info.copy()
419+
session["sub"] = "doe"
420+
session["uid"] = "diana"
421+
session["authn_event"] = create_authn_event("diana", "salt")
422+
423+
res = collect_user_info(self.endpoint_context, session)
424+
425+
assert res == {
426+
'email': 'diana@example.org',
427+
'email_verified': False,
428+
'nickname': 'Dina',
429+
'given_name': 'Diana',
430+
'sub': 'doe'
431+
}
432+
433+
def test_collect_user_info_2(self):
434+
_req = OIDR.copy()
435+
_req["scope"] = "openid email"
436+
del _req["claims"]
437+
438+
_session_info = {"authn_req": _req}
439+
session = _session_info.copy()
440+
session["sub"] = "doe"
441+
session["uid"] = "diana"
442+
session["authn_event"] = create_authn_event("diana", "salt")
443+
444+
self.endpoint_context.provider_info["claims_supported"].remove("email")
445+
self.endpoint_context.provider_info["claims_supported"].remove("email_verified")
446+
447+
res = collect_user_info(self.endpoint_context, session)
448+
449+
assert res == {
450+
"sub": "doe",
451+
"email": "diana@example.org",
452+
"email_verified": False,
453+
}
454+
455+
def test_collect_user_info_scope_not_supported(self):
456+
_req = OIDR.copy()
457+
_req["scope"] = "openid email address"
458+
del _req["claims"]
459+
460+
_session_info = {"authn_req": _req}
461+
session = _session_info.copy()
462+
session["sub"] = "doe"
463+
session["uid"] = "diana"
464+
session["authn_event"] = create_authn_event("diana", "salt")
465+
466+
# Scope address not supported
467+
self.endpoint_context.provider_info["scopes_supported"] = [
468+
"openid", "email", "offline_access"
469+
]
470+
res = collect_user_info(self.endpoint_context, session)
471+
472+
assert res == {
473+
"sub": "doe",
474+
"email": "diana@example.org",
475+
"email_verified": False,
476+
}

tests/test_22_oidc_provider_config_endpoint.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import json
22

33
import pytest
4+
45
from oidcendpoint.endpoint_context import EndpointContext
56
from oidcendpoint.oidc.provider_config import ProviderConfiguration
67
from oidcendpoint.oidc.token import AccessToken
@@ -77,4 +78,10 @@ def test_do_response(self):
7778
assert _msg
7879
assert _msg["token_endpoint"] == "https://example.com/token"
7980
assert _msg["jwks_uri"] == "https://example.com/static/jwks.json"
81+
assert set(_msg["claims_supported"]) == {
82+
'gender', 'zoneinfo', 'website', 'phone_number_verified', 'middle_name', 'family_name',
83+
'nickname', 'email', 'preferred_username', 'profile', 'name', 'phone_number',
84+
'given_name', 'email_verified', 'sub', 'locale', 'picture', 'address', 'updated_at',
85+
'birthdate'
86+
}
8087
assert ("Content-type", "application/json") in msg["http_headers"]

tests/test_26_oidc_userinfo_endpoint.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33

44
import pytest
55
from cryptojwt.jwt import utc_time_sans_frac
6+
from oidcmsg.oidc import AccessTokenRequest
7+
from oidcmsg.oidc import AuthorizationRequest
8+
69
from oidcendpoint import user_info
710
from oidcendpoint.endpoint_context import EndpointContext
811
from oidcendpoint.id_token import IDToken
@@ -14,8 +17,6 @@
1417
from oidcendpoint.session import setup_session
1518
from oidcendpoint.user_authn.authn_context import INTERNETPROTOCOLPASSWORD
1619
from oidcendpoint.user_info import UserInfo
17-
from oidcmsg.oidc import AccessTokenRequest
18-
from oidcmsg.oidc import AuthorizationRequest
1920

2021
KEYDEFS = [
2122
{"type": "RSA", "key": "", "use": ["sig"]},
@@ -168,6 +169,11 @@ def create_endpoint(self):
168169

169170
def test_init(self):
170171
assert self.endpoint
172+
assert set(self.endpoint.endpoint_context.provider_info["claims_supported"]) == {
173+
'iss', 'picture', 'birthdate', 'middle_name', 'zoneinfo', 'locale', 'address', 'gender',
174+
'email', 'nickname', 'phone_number_verified', 'sub', 'phone_number', 'name',
175+
'family_name', 'preferred_username', 'email_verified', 'updated_at', 'website',
176+
'given_name', 'eduperson_scoped_affiliation', 'profile'}
171177

172178
def test_parse(self):
173179
session_id = setup_session(

0 commit comments

Comments
 (0)