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

Commit 8de3acf

Browse files
authored
Merge pull request #182 from IdentityPython/develop
A number of bug fixes and new functionality
2 parents c2e09be + 7407cfa commit 8de3acf

File tree

11 files changed

+382
-17
lines changed

11 files changed

+382
-17
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ It also comes with the following `add_on` modules.
3232
* [OAuth2 PAR](https://datatracker.ietf.org/doc/html/rfc9126)
3333
* [OAuth2 RAR](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-rar)
3434
* [OAuth2 DPoP](https://tools.ietf.org/id/draft-fett-oauth-dpop-04.html)
35+
* [OAuth 2.0 Authorization Server Issuer Identification](https://datatracker.ietf.org/doc/draft-ietf-oauth-iss-auth-resp)
3536

3637
The entire project code is open sourced and therefore licensed under the [Apache 2.0](https://en.wikipedia.org/wiki/Apache_License)
3738

example/flask_op/server.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
44
import logging
55
import os
66

7-
from oidcop.configure import Configuration
7+
from oidcmsg.configure import Configuration
8+
from oidcmsg.configure import create_from_config_file
9+
810
from oidcop.configure import OPConfiguration
9-
from oidcop.configure import create_from_config_file
1011
from oidcop.utils import create_context
1112

1213
try:
@@ -62,7 +63,7 @@ def main(config_file, args):
6263
app = oidc_provider_init_app(config.op, 'oidc_op')
6364
app.logger = config.logger
6465

65-
web_conf = config.webserver
66+
web_conf = config.web_conf
6667

6768
context = create_context(dir_path, web_conf)
6869

src/oidcop/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import secrets
22

3-
__version__ = "2.3.4"
3+
__version__ = "2.4.0"
44

55
DEF_SIGN_ALG = {
66
"id_token": "RS256",

src/oidcop/endpoint.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import json
12
import logging
23
from typing import Callable
34
from typing import Optional
@@ -363,7 +364,10 @@ def do_response(
363364
if self.response_placement == "body":
364365
if self.response_format == "json":
365366
content_type = "application/json; charset=utf-8"
366-
resp = _response.to_json()
367+
if isinstance(_response, Message):
368+
resp = _response.to_json()
369+
else:
370+
resp = json.dumps(_response)
367371
elif self.response_format in ["jws", "jwe", "jose"]:
368372
content_type = "application/jose; charset=utf-8"
369373
resp = _response

src/oidcop/endpoint_context.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -119,13 +119,13 @@ class EndpointContext(OidcContext):
119119
}
120120

121121
def __init__(
122-
self,
123-
conf: Union[dict, OPConfiguration],
124-
server_get: Callable,
125-
keyjar: Optional[KeyJar] = None,
126-
cwd: Optional[str] = "",
127-
cookie_handler: Optional[Any] = None,
128-
httpc: Optional[Any] = None,
122+
self,
123+
conf: Union[dict, OPConfiguration],
124+
server_get: Callable,
125+
keyjar: Optional[KeyJar] = None,
126+
cwd: Optional[str] = "",
127+
cookie_handler: Optional[Any] = None,
128+
httpc: Optional[Any] = None,
129129
):
130130
OidcContext.__init__(self, conf, keyjar, entity_id=conf.get("issuer", ""))
131131
self.conf = conf
@@ -148,6 +148,7 @@ def __init__(
148148

149149
# Default values, to be changed below depending on configuration
150150
# arguments for endpoints add-ons
151+
self.add_on = {}
151152
self.args = {}
152153
self.authn_broker = None
153154
self.authz = None
@@ -161,7 +162,7 @@ def __init__(
161162
self.login_hint2acrs = None
162163
self.par_db = {}
163164
self.provider_info = {}
164-
self.scope2claims = SCOPE2CLAIMS
165+
self.scope2claims = conf.get("scopes_to_claims", SCOPE2CLAIMS)
165166
self.session_manager = None
166167
self.sso_ttl = 14400 # 4h
167168
self.symkey = rndstr(24)
@@ -215,7 +216,6 @@ def __init__(
215216
"cookie_handler",
216217
"authentication",
217218
"id_token",
218-
"scope2claims",
219219
]:
220220
_func = getattr(self, "do_{}".format(item), None)
221221
if _func:
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
def pre_construct(response_args, request, endpoint_context, **kwargs):
2+
"""
3+
Add extra arguments to the request.
4+
5+
:param response_args:
6+
:param request:
7+
:param endpoint_context:
8+
:param kwargs:
9+
:return:
10+
"""
11+
12+
_extra = endpoint_context.add_on.get("extra_args")
13+
if _extra:
14+
for arg, _param in _extra.items():
15+
_val = endpoint_context.get(_param)
16+
if _val:
17+
request[arg] = _val
18+
19+
return request
20+
21+
22+
def add_support(endpoint, **kwargs):
23+
#
24+
_added = False
25+
for endpoint_name in list(kwargs.keys()):
26+
_endp = endpoint[endpoint_name]
27+
_endp.pre_construct.append(pre_construct)
28+
29+
if _added is False:
30+
_endp.server_get("endpoint_context").add_on["extra_args"] = kwargs
31+
_added = True

src/oidcop/session/claims.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,10 @@ def get_claims_from_request(
117117
_always_add = module.kwargs.get("always_add_claims", {})
118118

119119
if _always_add:
120-
base_claims.update({k: None for k in _always_add})
120+
if isinstance(_always_add, list):
121+
base_claims.update({k: None for k in _always_add})
122+
else:
123+
base_claims.update(_always_add)
121124

122125
if _claims_by_scope:
123126
if scopes is None:

src/oidcop/session/grant.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ def payload_arguments(
227227
if self.authorization_request:
228228
client_id = self.authorization_request.get("client_id")
229229
if client_id:
230-
payload.update({"client_id": client_id, "sub": client_id})
230+
payload.update({"client_id": client_id, "sub": self.sub})
231231

232232
_claims_restriction = endpoint_context.claims_interface.get_claims(
233233
session_id,

tests/test_05_jwt_token.py

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from oidcmsg.oidc import AccessTokenRequest
77
from oidcmsg.oidc import AuthorizationRequest
88
from oidcmsg.time_util import utc_time_sans_frac
9+
from oidcop.scopes import SCOPE2CLAIMS
910

1011
from oidcop import user_info
1112
from oidcop.authn_event import create_authn_event
@@ -275,3 +276,157 @@ def test_is_expired(self):
275276
assert access_token.is_active()
276277
# 4000 seconds in the future. Passed the lifetime.
277278
assert access_token.is_active(now=utc_time_sans_frac() + 4000) is False
279+
280+
281+
class TestEndpointWebID(object):
282+
@pytest.fixture(autouse=True)
283+
def create_endpoint(self):
284+
_scope2claims = SCOPE2CLAIMS.copy()
285+
_scope2claims.update({"webid": ["webid"]})
286+
conf = {
287+
"issuer": ISSUER,
288+
"httpc_params": {"verify": False, "timeout": 1},
289+
"capabilities": CAPABILITIES,
290+
"keys": {"uri_path": "jwks.json", "key_defs": KEYDEFS},
291+
"token_handler_args": {
292+
"jwks_file": "private/token_jwks.json",
293+
"code": {"lifetime": 600},
294+
"token": {
295+
"class": "oidcop.token.jwt_token.JWTToken",
296+
"kwargs": {
297+
"lifetime": 3600,
298+
"base_claims": {"eduperson_scoped_affiliation": None},
299+
"add_claims_by_scope": True,
300+
"aud": ["https://example.org/appl"],
301+
},
302+
},
303+
"refresh": {
304+
"class": "oidcop.token.jwt_token.JWTToken",
305+
"kwargs": {"lifetime": 3600, "aud": ["https://example.org/appl"], },
306+
},
307+
"id_token": {
308+
"class": "oidcop.token.id_token.IDToken",
309+
"kwargs": {
310+
"base_claims": {
311+
"email": {"essential": True},
312+
"email_verified": {"essential": True},
313+
}
314+
},
315+
},
316+
},
317+
"endpoint": {
318+
"provider_config": {
319+
"path": "{}/.well-known/openid-configuration",
320+
"class": ProviderConfiguration,
321+
"kwargs": {},
322+
},
323+
"registration": {"path": "{}/registration", "class": Registration, "kwargs": {}, },
324+
"authorization": {
325+
"path": "{}/authorization",
326+
"class": Authorization,
327+
"kwargs": {},
328+
},
329+
"token": {"path": "{}/token", "class": Token, "kwargs": {}},
330+
"session": {"path": "{}/end_session", "class": Session},
331+
"introspection": {"path": "{}/introspection", "class": Introspection},
332+
},
333+
"client_authn": verify_client,
334+
"authentication": {
335+
"anon": {
336+
"acr": INTERNETPROTOCOLPASSWORD,
337+
"class": "oidcop.user_authn.user.NoAuthn",
338+
"kwargs": {"user": "diana"},
339+
}
340+
},
341+
"template_dir": "template",
342+
"userinfo": {
343+
"class": user_info.UserInfo,
344+
"kwargs": {"db_file": full_path("users.json")},
345+
},
346+
"authz": {
347+
"class": AuthzHandling,
348+
"kwargs": {
349+
"grant_config": {
350+
"usage_rules": {
351+
"authorization_code": {
352+
"supports_minting": ["access_token", "refresh_token", "id_token", ],
353+
"max_usage": 1,
354+
},
355+
"access_token": {},
356+
"refresh_token": {
357+
"supports_minting": ["access_token", "refresh_token"],
358+
},
359+
},
360+
"expires_in": 43200,
361+
}
362+
},
363+
},
364+
"claims_interface": {"class": "oidcop.session.claims.ClaimsInterface", "kwargs": {}},
365+
"scopes_to_claims": _scope2claims,
366+
}
367+
server = Server(conf, keyjar=KEYJAR)
368+
self.endpoint_context = server.endpoint_context
369+
self.endpoint_context.cdb["client_1"] = {
370+
"client_secret": "hemligt",
371+
"redirect_uris": [("https://example.com/cb", None)],
372+
"client_salt": "salted",
373+
"token_endpoint_auth_method": "client_secret_post",
374+
"response_types": ["code", "token", "code id_token", "id_token"],
375+
"add_claims": {
376+
"always": {},
377+
"by_scope": {},
378+
},
379+
}
380+
self.session_manager = self.endpoint_context.session_manager
381+
self.user_id = "diana"
382+
self.endpoint = server.server_get("endpoint", "session")
383+
384+
def _create_session(self, auth_req, sub_type="public", sector_identifier=""):
385+
if sector_identifier:
386+
authz_req = auth_req.copy()
387+
authz_req["sector_identifier_uri"] = sector_identifier
388+
else:
389+
authz_req = auth_req
390+
client_id = authz_req["client_id"]
391+
ae = create_authn_event(self.user_id)
392+
return self.session_manager.create_session(
393+
ae, authz_req, self.user_id, client_id=client_id, sub_type=sub_type
394+
)
395+
396+
def _mint_token(self, token_class, grant, session_id, based_on=None, **kwargs):
397+
# Constructing an authorization code is now done
398+
return grant.mint_token(
399+
session_id=session_id,
400+
endpoint_context=self.endpoint_context,
401+
token_class=token_class,
402+
token_handler=self.session_manager.token_handler.handler[token_class],
403+
expires_at=utc_time_sans_frac() + 300, # 5 minutes from now
404+
based_on=based_on,
405+
**kwargs
406+
)
407+
408+
def test_parse(self):
409+
_auth_req = AuthorizationRequest(
410+
client_id="client_1",
411+
redirect_uri="https://example.com/cb",
412+
scope=["openid", "webid"],
413+
state="STATE",
414+
response_type="code",
415+
)
416+
417+
session_id = self._create_session(_auth_req)
418+
# apply consent
419+
grant = self.endpoint_context.authz(session_id=session_id, request=_auth_req)
420+
# grant = self.session_manager[session_id]
421+
code = self._mint_token("authorization_code", grant, session_id)
422+
access_token = self._mint_token(
423+
"access_token", grant, session_id, code, resources=[_auth_req["client_id"]]
424+
)
425+
426+
_verifier = JWT(self.endpoint_context.keyjar)
427+
_info = _verifier.unpack(access_token.value)
428+
429+
assert _info["token_class"] == "access_token"
430+
# assert _info["eduperson_scoped_affiliation"] == ["[email protected]"]
431+
assert set(_info["aud"]) == {"client_1"}
432+
assert "webid" in _info

0 commit comments

Comments
 (0)