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

Commit 768dc30

Browse files
authored
Merge branch 'develop' into certification
2 parents 984eb5a + d920971 commit 768dc30

23 files changed

Lines changed: 391 additions & 47 deletions

docs/source/contents/conf.rst

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,27 @@ An example::
8484
}
8585
}
8686

87+
The provided add-ons can be seen in the following sections.
88+
89+
pkce
90+
####
91+
92+
The pkce add on is activated using the ``oidcop.oidc.add_on.pkce.add_pkce_support``
93+
function. The possible configuration options can be found below.
94+
95+
essential
96+
---------
97+
98+
Whether pkce is mandatory, authentication requests without a ``code_challenge``
99+
will fail if this is True. This option can be overridden per client by defining
100+
``pkce_essential`` in the client metadata.
101+
102+
code_challenge_method
103+
---------------------
104+
105+
The allowed code_challenge methods. The supported code challenge methods are:
106+
``plain, S256, S384, S512``
107+
87108
--------------
88109
authentication
89110
--------------
@@ -622,3 +643,80 @@ the following::
622643
}
623644
}
624645
}
646+
647+
648+
=======
649+
Clients
650+
=======
651+
652+
In this section there are some client configuration examples.
653+
654+
A common configuration::
655+
656+
endpoint_context.cdb['jbxedfmfyc'] = {
657+
client_id: 'jbxedfmfyc',
658+
client_salt: '6flfsj0Z',
659+
registration_access_token: 'z3PCMmC1HZ1QmXeXGOQMJpWQNQynM4xY',
660+
registration_client_uri: 'https://127.0.0.1:8000/registration_api?client_id=jbxedfmfyc',
661+
client_id_issued_at: 1630256902,
662+
client_secret: '19cc69b70d0108f630e52f72f7a3bd37ba4e11678ad1a7434e9818e1',
663+
client_secret_expires_at: 1929727754,
664+
application_type: 'web',
665+
contacts: [
666+
'rp@example.com'
667+
],
668+
token_endpoint_auth_method: 'client_secret_basic',
669+
redirect_uris: [
670+
[
671+
'https://127.0.0.1:8090/authz_cb/satosa',
672+
{}
673+
]
674+
],
675+
post_logout_redirect_uris: [
676+
[
677+
'https://127.0.0.1:8090/session_logout/satosa',
678+
null
679+
]
680+
],
681+
response_types: [
682+
'code'
683+
],
684+
grant_types: [
685+
'authorization_code'
686+
],
687+
allowed_scopes: [
688+
'openid',
689+
'profile',
690+
'email',
691+
'offline_access'
692+
]
693+
}
694+
695+
696+
How to configure the release of the user claims per clients::
697+
698+
endpoint_context.cdb["client_1"] = {
699+
"client_secret": "hemligt",
700+
"redirect_uris": [("https://example.com/cb", None)],
701+
"client_salt": "salted",
702+
"token_endpoint_auth_method": "client_secret_post",
703+
"response_types": ["code", "token", "code id_token", "id_token"],
704+
"add_claims": {
705+
"always": {
706+
"introspection": ["nickname", "eduperson_scoped_affiliation"],
707+
"userinfo": ["picture", "phone_number"],
708+
},
709+
# this overload the general endpoint configuration for this client
710+
# self.server.server_get("endpoint", "id_token").kwargs = {"add_claims_by_scope": True}
711+
"by_scope": {
712+
"id_token": False,
713+
},
714+
},
715+
716+
Some of the allowed client configurations are (this is an ongoing work):
717+
718+
---------------------
719+
grant_types_supported
720+
---------------------
721+
722+
Configure the allowed grant types on the token endpoint.

docs/source/contents/usage.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
Usage
22
-----
33

4-
Some examples, how to run flask_op and django_op, but also some typical configuration in relation to common use cases.
4+
Some examples, how to run [flask_op](https://github.com/IdentityPython/oidc-op/tree/master/example/flask_op) and [django_op](https://github.com/peppelinux/django-oidc-op) but also some typical configuration in relation to common use cases.
55

66

77

@@ -34,7 +34,7 @@ Get to the RP landing page to choose your authentication endpoint. The first opt
3434

3535
![OP Auth](../_images/2.png)
3636

37-
AS/OP accepted our authentication request and prompt to us the login form. Read passwd.json file to get credentials.
37+
The AS/OP supports dynamic client registration, it accepts the authentication request and prompt to us the login form. Read [passwd.json](https://github.com/IdentityPython/oidc-op/blob/master/example/flask_op/passwd.json) file to get credentials.
3838

3939
----------------------------------
4040

@@ -75,12 +75,12 @@ It is important to consider that only scope=offline_access will get a usable ref
7575

7676
oidc-op will return a json response like this::
7777

78-
{
79-
'access_token': 'eyJhbGc ... CIOH_09tT_YVa_gyTqg',
80-
'token_type': 'Bearer',
81-
'scope': 'openid profile email address phone offline_access',
82-
'refresh_token': 'Z0FBQ ... 1TE16cm1Tdg=='
83-
}
78+
{
79+
'access_token': 'eyJhbGc ... CIOH_09tT_YVa_gyTqg',
80+
'token_type': 'Bearer',
81+
'scope': 'openid profile email address phone offline_access',
82+
'refresh_token': 'Z0FBQ ... 1TE16cm1Tdg=='
83+
}
8484

8585

8686

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.1.0"
3+
__version__ = "2.1.1"
44

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

src/oidcop/oauth2/token.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
from cryptojwt.jwe.exception import JWEException
66
from cryptojwt.jwt import utc_time_sans_frac
7-
87
from oidcmsg.message import Message
98
from oidcmsg.oauth2 import AccessTokenResponse
109
from oidcmsg.oauth2 import ResponseMessage
@@ -403,13 +402,20 @@ def configure_grant_types(self, grant_types_supported):
403402
def _post_parse_request(
404403
self, request: Union[Message, dict], client_id: Optional[str] = "", **kwargs
405404
):
406-
_helper = self.helper.get(request["grant_type"])
405+
grant_type = request["grant_type"]
406+
_helper = self.helper.get(grant_type)
407+
client = kwargs["endpoint_context"].cdb[client_id]
408+
if "grant_types_supported" in client and grant_type not in client["grant_types_supported"]:
409+
return self.error_cls(
410+
error="invalid_request",
411+
error_description=f"Unsupported grant_type: {grant_type}",
412+
)
407413
if _helper:
408414
return _helper.post_parse_request(request, client_id, **kwargs)
409415
else:
410416
return self.error_cls(
411417
error="invalid_request",
412-
error_description=f"Unsupported grant_type: {request['grant_type']}",
418+
error_description=f"Unsupported grant_type: {grant_type}",
413419
)
414420

415421
def process_request(self, request: Optional[Union[Message, dict]] = None, **kwargs):

src/oidcop/oidc/add_on/pkce.py

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,9 @@
33
from typing import Dict
44

55
from cryptojwt.utils import b64e
6-
from oidcmsg.oauth2 import (
7-
AuthorizationErrorResponse,
8-
RefreshAccessTokenRequest,
9-
TokenExchangeRequest,
10-
)
6+
from oidcmsg.oauth2 import AuthorizationErrorResponse
7+
from oidcmsg.oauth2 import RefreshAccessTokenRequest
8+
from oidcmsg.oauth2 import TokenExchangeRequest
119
from oidcmsg.oidc import TokenErrorResponse
1210

1311
from oidcop.endpoint import Endpoint
@@ -41,7 +39,14 @@ def post_authn_parse(request, client_id, endpoint_context, **kwargs):
4139
:param kwargs:
4240
:return:
4341
"""
44-
if endpoint_context.args["pkce"]["essential"] and "code_challenge" not in request:
42+
client = endpoint_context.cdb[client_id]
43+
if "pkce_essential" in client:
44+
essential = client["pkce_essential"]
45+
else:
46+
essential = endpoint_context.args["pkce"].get(
47+
"essential", False
48+
)
49+
if essential and "code_challenge" not in request:
4550
return AuthorizationErrorResponse(
4651
error="invalid_request", error_description="Missing required code_challenge",
4752
)
@@ -131,9 +136,6 @@ def add_pkce_support(endpoint: Dict[str, Endpoint], **kwargs):
131136
authn_endpoint.post_parse_request.append(post_authn_parse)
132137
token_endpoint.post_parse_request.append(post_token_parse)
133138

134-
if "essential" not in kwargs:
135-
kwargs["essential"] = False
136-
137139
code_challenge_methods = kwargs.get("code_challenge_methods", CC_METHOD.keys())
138140

139141
kwargs["code_challenge_methods"] = {}

src/oidcop/session/claims.py

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from oidcmsg.oidc import OpenIDSchema
66

77
from oidcop.exception import ServiceError
8+
from oidcop.exception import ImproperlyConfigured
89
from oidcop.scopes import convert_scopes2claims
910

1011
logger = logging.getLogger(__name__)
@@ -41,7 +42,11 @@ def authorization_request_claims(self,
4142

4243
def _get_client_claims(self, client_id, usage):
4344
client_info = self.server_get("endpoint_context").cdb.get(client_id, {})
44-
client_claims = client_info.get("{}_claims".format(usage), {})
45+
client_claims = (
46+
client_info.get("add_claims", {})
47+
.get("always", {})
48+
.get(usage, {})
49+
)
4550
if isinstance(client_claims, list):
4651
client_claims = {k: None for k in client_claims}
4752
return client_claims
@@ -94,8 +99,19 @@ def get_claims(self, session_id: str, scopes: str, claims_release_point: str) ->
9499

95100
claims.update(base_claims)
96101

97-
# Scopes can in some cases equate to set of claims, is that used here ?
98-
if module.kwargs.get("add_claims_by_scope"):
102+
# If specific client configuration exists overwrite add_claims_by_scope
103+
if client_id in _context.cdb:
104+
add_claims_by_scope = (
105+
_context.cdb[client_id].get("add_claims", {})
106+
.get("by_scope", {})
107+
.get(claims_release_point, {})
108+
)
109+
if isinstance(add_claims_by_scope, dict) and not add_claims_by_scope:
110+
add_claims_by_scope = module.kwargs.get("add_claims_by_scope")
111+
else:
112+
add_claims_by_scope = module.kwargs.get("add_claims_by_scope")
113+
114+
if add_claims_by_scope:
99115
if scopes:
100116
_scopes = _context.scopes_handler.filter_scopes(client_id, _context, scopes)
101117

@@ -127,9 +143,14 @@ def get_user_claims(self, user_id: str, claims_restriction: dict) -> dict:
127143
:param claims_restriction: Specifies the upper limit of which claims can be returned
128144
:return:
129145
"""
146+
meth = self.server_get("endpoint_context").userinfo
147+
if not meth:
148+
raise ImproperlyConfigured(
149+
"userinfo MUST be defined in the configuration"
150+
)
130151
if claims_restriction:
131152
# Get all possible claims
132-
user_info = self.server_get("endpoint_context").userinfo(user_id, client_id=None)
153+
user_info = meth(user_id, client_id=None)
133154
# Filter out the claims that can be returned
134155
return {
135156
k: user_info.get(k)

src/oidcop/session/grant.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,9 @@ def mint_token(
316316
scope=scope,
317317
extra_payload=handler_args,
318318
)
319-
item.value = token_handler(session_id=session_id, **token_payload)
319+
item.value = token_handler(
320+
session_id=session_id, usage_rules=usage_rules, **token_payload
321+
)
320322

321323
else:
322324
raise ValueError("Can not mint that kind of token")

src/oidcop/token/id_token.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,7 @@ def __call__(
267267
encrypt=False,
268268
code=None,
269269
access_token=None,
270+
usage_rules: Optional[dict] = None,
270271
**kwargs,
271272
) -> str:
272273
_context = self.server_get("endpoint_context")

src/oidcop/token/jwt_token.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,13 @@ def load_custom_claims(self, payload: dict = None):
4646
# inherit me and do your things here
4747
return payload
4848

49-
def __call__(self, session_id: Optional[str] = "", token_class: Optional[str] = "",
50-
**payload) -> str:
49+
def __call__(
50+
self,
51+
session_id: Optional[str] = "",
52+
token_class: Optional[str] = "",
53+
usage_rules: Optional[dict] = None,
54+
**payload
55+
) -> str:
5156

5257
"""
5358
Return a token.
@@ -68,8 +73,15 @@ def __call__(self, session_id: Optional[str] = "", token_class: Optional[str] =
6873

6974
# payload.update(kwargs)
7075
_context = self.server_get("endpoint_context")
76+
if usage_rules and "expires_in" in usage_rules:
77+
lifetime = usage_rules.get("expires_in")
78+
else:
79+
lifetime = self.lifetime
7180
signer = JWT(
72-
key_jar=_context.keyjar, iss=self.issuer, lifetime=self.lifetime, sign_alg=self.alg,
81+
key_jar=_context.keyjar,
82+
iss=self.issuer,
83+
lifetime=lifetime,
84+
sign_alg=self.alg,
7385
)
7486

7587
return signer.pack(payload)

tests/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import os
2+
3+
BASEDIR = os.path.abspath(os.path.dirname(__file__))
4+
5+
6+
def full_path(local_file):
7+
return os.path.join(BASEDIR, local_file)

0 commit comments

Comments
 (0)