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
Show all changes
27 commits
Select commit Hold shift + click to select a range
692a7f2
feat: added pypi gh action CD
peppelinux Jul 5, 2021
8181e3c
Merge pull request #110 from IdentityPython/pypi
peppelinux Jul 6, 2021
6dd6b4b
For debugging purpose nice to know what was put in the ID Token and a…
rohe Jul 8, 2021
ec7b7d2
Merge pull request #112 from IdentityPython/log_id_token
rohe Jul 10, 2021
7ca060d
Don't create a new client session every time
nsklikas Jul 27, 2021
10058ed
Merge pull request #114 from nsklikas/fix-client-session
peppelinux Jul 30, 2021
9c0f7db
Add pkce per client
nsklikas Aug 4, 2021
ccd7234
Fix bug with refresh id tokens
nsklikas Aug 4, 2021
fca92e7
Fix id tokens claims
nsklikas Aug 5, 2021
bf8a63b
Add documentation
nsklikas Aug 5, 2021
ee87c96
Fix bugs with refresh tokens
nsklikas Aug 4, 2021
9429d57
Handle ToOld token exception
nsklikas Aug 16, 2021
230d04e
Don't set default value to pkce essential
nsklikas Aug 16, 2021
852e710
Merge pull request #116 from nsklikas/fix-refresh-id-token
peppelinux Aug 17, 2021
6e0b9bf
Merge pull request #118 from nsklikas/fix-expired-token-response
peppelinux Aug 17, 2021
ac9d69f
chore: Exception when configuration lacks of userinfo definition
peppelinux Aug 19, 2021
600c03a
test: missing userinfo in configuration
peppelinux Aug 19, 2021
d0eb303
chore: fixed userinfo configuration in unit tests
peppelinux Aug 19, 2021
4c4e1e0
Fix JWT access token lifetime
nsklikas Aug 16, 2021
a84eadf
Introduce add_claims_by_scope per client configuration
ctriant Jul 27, 2021
630258b
Merge pull request #120 from IdentityPython/ui_excp
peppelinux Sep 2, 2021
78cae68
chore: Documentation on CDB and minor changes
peppelinux Sep 2, 2021
ce04493
Merge pull request #113 from ctriant/per_client_claims_by_scope
peppelinux Sep 2, 2021
a98696e
Merge branch 'develop' into pkce-per-client
peppelinux Sep 2, 2021
0be7aa4
Merge pull request #115 from nsklikas/pkce-per-client
peppelinux Sep 2, 2021
7bbde28
Merge pull request #117 from nsklikas/feature-jwt-access-lifetime
peppelinux Sep 2, 2021
906a1be
v2.1.1
peppelinux Sep 2, 2021
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
35 changes: 35 additions & 0 deletions .github/workflows/pypi.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
name: Publish Python distribution to PyPI
on:
release:
types:
- created

jobs:
build-n-publish:
name: Publish Python distribution to PyPI
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- name: Setup Python 3.8
uses: actions/setup-python@v1
with:
python-version: 3.8
- name: Install pypa/build
run: >-
python -m
pip install
build
--user
- name: Build a binary wheel and a source tarball
run: >-
python -m
build
--sdist
--wheel
--outdir dist/
.
- name: Publish distribution to PyPI
uses: pypa/gh-action-pypi-publish@master
with:
user: __token__
password: ${{ secrets.PYPI_API_TOKEN }}
90 changes: 90 additions & 0 deletions docs/source/contents/conf.rst
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,27 @@ An example::
}
}

The provided add-ons can be seen in the following sections.

pkce
####

The pkce add on is activated using the ``oidcop.oidc.add_on.pkce.add_pkce_support``
function. The possible configuration options can be found below.

essential
---------

Whether pkce is mandatory, authentication requests without a ``code_challenge``
will fail if this is True. This option can be overridden per client by defining
``pkce_essential`` in the client metadata.

code_challenge_method
---------------------

The allowed code_challenge methods. The supported code challenge methods are:
``plain, S256, S384, S512``

--------------
authentication
--------------
Expand Down Expand Up @@ -622,3 +643,72 @@ the following::
}
}
}


=======
Clients
=======

In this section there are some client configuration examples.

A common configuration::

endpoint_context.cdb['jbxedfmfyc'] = {
client_id: 'jbxedfmfyc',
client_salt: '6flfsj0Z',
registration_access_token: 'z3PCMmC1HZ1QmXeXGOQMJpWQNQynM4xY',
registration_client_uri: 'https://127.0.0.1:8000/registration_api?client_id=jbxedfmfyc',
client_id_issued_at: 1630256902,
client_secret: '19cc69b70d0108f630e52f72f7a3bd37ba4e11678ad1a7434e9818e1',
client_secret_expires_at: 1929727754,
application_type: 'web',
contacts: [
'rp@example.com'
],
token_endpoint_auth_method: 'client_secret_basic',
redirect_uris: [
[
'https://127.0.0.1:8090/authz_cb/satosa',
{}
]
],
post_logout_redirect_uris: [
[
'https://127.0.0.1:8090/session_logout/satosa',
null
]
],
response_types: [
'code'
],
grant_types: [
'authorization_code'
],
allowed_scopes: [
'openid',
'profile',
'email',
'offline_access'
]
}


How to configure the release of the user claims per clients::

endpoint_context.cdb["client_1"] = {
"client_secret": "hemligt",
"redirect_uris": [("https://example.com/cb", None)],
"client_salt": "salted",
"token_endpoint_auth_method": "client_secret_post",
"response_types": ["code", "token", "code id_token", "id_token"],
"add_claims": {
"always": {
"introspection": ["nickname", "eduperson_scoped_affiliation"],
"userinfo": ["picture", "phone_number"],
},
# this overload the general endpoint configuration for this client
# self.server.server_get("endpoint", "id_token").kwargs = {"add_claims_by_scope": True}
"by_scope": {
"id_token": False,
},
},
16 changes: 8 additions & 8 deletions docs/source/contents/usage.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Usage
-----

Some examples, how to run flask_op and django_op, but also some typical configuration in relation to common use cases.
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.



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

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

AS/OP accepted our authentication request and prompt to us the login form. Read passwd.json file to get credentials.
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.

----------------------------------

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

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

{
'access_token': 'eyJhbGc ... CIOH_09tT_YVa_gyTqg',
'token_type': 'Bearer',
'scope': 'openid profile email address phone offline_access',
'refresh_token': 'Z0FBQ ... 1TE16cm1Tdg=='
}
{
'access_token': 'eyJhbGc ... CIOH_09tT_YVa_gyTqg',
'token_type': 'Bearer',
'scope': 'openid profile email address phone offline_access',
'refresh_token': 'Z0FBQ ... 1TE16cm1Tdg=='
}



Expand Down
1 change: 1 addition & 0 deletions example/flask_op/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ def _add_cookie(resp, cookie_spec):
for k,v in cookie_spec.items()
if k not in ('name',)}
kwargs["path"] = "/"
kwargs["samesite"] = "Lax"
resp.set_cookie(cookie_spec["name"], **kwargs)


Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
oidcmsg>=1.3.0
oidcmsg>=1.4.0
pyyaml
jinja2>=2.11.3
responses>=0.13.0
2 changes: 1 addition & 1 deletion src/oidcop/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import secrets

__version__ = "2.1.0"
__version__ = "2.1.1"

DEF_SIGN_ALG = {
"id_token": "RS256",
Expand Down
3 changes: 3 additions & 0 deletions src/oidcop/client_authn.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from oidcop.exception import InvalidClient
from oidcop.exception import MultipleUsage
from oidcop.exception import NotForMe
from oidcop.exception import ToOld
from oidcop.exception import UnknownClient
from oidcop.util import importer

Expand Down Expand Up @@ -409,6 +410,8 @@ def verify_client(
try:
# get_client_id_from_token is a callback... Do not abuse for code readability.
auth_info["client_id"] = get_client_id_from_token(endpoint_context, _token, request)
except ToOld:
raise ValueError("Expired token")
except KeyError:
raise ValueError("Unknown token")

Expand Down
4 changes: 2 additions & 2 deletions src/oidcop/oauth2/token.py
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ def process_request(self, req: Union[Message, dict], **kwargs):
_resp = {
"access_token": access_token.value,
"token_type": access_token.token_type,
"scope": _grant.scope,
"scope": scope,
}

if access_token.expires_at:
Expand Down Expand Up @@ -318,7 +318,7 @@ def post_parse_request(
if "scope" in request:
req_scopes = set(request["scope"])
scopes = set(grant.find_scope(token.based_on))
if scopes < req_scopes:
if not req_scopes.issubset(scopes):
return self.error_cls(
error="invalid_request",
error_description="Invalid refresh scopes",
Expand Down
20 changes: 11 additions & 9 deletions src/oidcop/oidc/add_on/pkce.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,9 @@
from typing import Dict

from cryptojwt.utils import b64e
from oidcmsg.oauth2 import (
AuthorizationErrorResponse,
RefreshAccessTokenRequest,
TokenExchangeRequest,
)
from oidcmsg.oauth2 import AuthorizationErrorResponse
from oidcmsg.oauth2 import RefreshAccessTokenRequest
from oidcmsg.oauth2 import TokenExchangeRequest
from oidcmsg.oidc import TokenErrorResponse

from oidcop.endpoint import Endpoint
Expand Down Expand Up @@ -41,7 +39,14 @@ def post_authn_parse(request, client_id, endpoint_context, **kwargs):
:param kwargs:
:return:
"""
if endpoint_context.args["pkce"]["essential"] and "code_challenge" not in request:
client = endpoint_context.cdb[client_id]
if "pkce_essential" in client:
essential = client["pkce_essential"]
else:
essential = endpoint_context.args["pkce"].get(
"essential", False
)
if essential and "code_challenge" not in request:
return AuthorizationErrorResponse(
error="invalid_request", error_description="Missing required code_challenge",
)
Expand Down Expand Up @@ -131,9 +136,6 @@ def add_pkce_support(endpoint: Dict[str, Endpoint], **kwargs):
authn_endpoint.post_parse_request.append(post_authn_parse)
token_endpoint.post_parse_request.append(post_token_parse)

if "essential" not in kwargs:
kwargs["essential"] = False

code_challenge_methods = kwargs.get("code_challenge_methods", CC_METHOD.keys())

kwargs["code_challenge_methods"] = {}
Expand Down
6 changes: 3 additions & 3 deletions src/oidcop/oidc/token.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ def process_request(self, req: Union[Message, dict], **kwargs):
_resp = {
"access_token": access_token.value,
"token_type": token_type,
"scope": _grant.scope,
"scope": scope,
}

if access_token.expires_at:
Expand Down Expand Up @@ -246,7 +246,7 @@ def process_request(self, req: Union[Message, dict], **kwargs):
if "id_token" in _mints and "openid" in scope:
try:
_idtoken = self._mint_token(
token_class="refresh_token",
token_class="id_token",
grant=_grant,
session_id=_session_info["session_id"],
client_id=_session_info["client_id"],
Expand Down Expand Up @@ -307,7 +307,7 @@ def post_parse_request(
if "scope" in request:
req_scopes = set(request["scope"])
scopes = set(grant.find_scope(token.based_on))
if scopes < req_scopes:
if not req_scopes.issubset(scopes):
return self.error_cls(
error="invalid_request",
error_description="Invalid refresh scopes",
Expand Down
29 changes: 25 additions & 4 deletions src/oidcop/session/claims.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from oidcmsg.oidc import OpenIDSchema

from oidcop.exception import ServiceError
from oidcop.exception import ImproperlyConfigured
from oidcop.scopes import convert_scopes2claims

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -41,7 +42,11 @@ def authorization_request_claims(self,

def _get_client_claims(self, client_id, usage):
client_info = self.server_get("endpoint_context").cdb.get(client_id, {})
client_claims = client_info.get("{}_claims".format(usage), {})
client_claims = (
client_info.get("add_claims", {})
.get("always", {})
.get(usage, {})
)
if isinstance(client_claims, list):
client_claims = {k: None for k in client_claims}
return client_claims
Expand Down Expand Up @@ -94,8 +99,19 @@ def get_claims(self, session_id: str, scopes: str, claims_release_point: str) ->

claims.update(base_claims)

# Scopes can in some cases equate to set of claims, is that used here ?
if module.kwargs.get("add_claims_by_scope"):
# If specific client configuration exists overwrite add_claims_by_scope
if client_id in _context.cdb:
add_claims_by_scope = (
_context.cdb[client_id].get("add_claims", {})
.get("by_scope", {})
.get(claims_release_point, {})
)
if isinstance(add_claims_by_scope, dict) and not add_claims_by_scope:
add_claims_by_scope = module.kwargs.get("add_claims_by_scope")
else:
add_claims_by_scope = module.kwargs.get("add_claims_by_scope")

if add_claims_by_scope:
if scopes:
_scopes = _context.scopes_handler.filter_scopes(client_id, _context, scopes)

Expand Down Expand Up @@ -127,9 +143,14 @@ def get_user_claims(self, user_id: str, claims_restriction: dict) -> dict:
:param claims_restriction: Specifies the upper limit of which claims can be returned
:return:
"""
meth = self.server_get("endpoint_context").userinfo
if not meth:
raise ImproperlyConfigured(
"userinfo MUST be defined in the configuration"
)
if claims_restriction:
# Get all possible claims
user_info = self.server_get("endpoint_context").userinfo(user_id, client_id=None)
user_info = meth(user_id, client_id=None)
# Filter out the claims that can be returned
return {
k: user_info.get(k)
Expand Down
4 changes: 3 additions & 1 deletion src/oidcop/session/grant.py
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,9 @@ def mint_token(
scope=scope,
extra_payload=handler_args,
)
item.value = token_handler(session_id=session_id, **token_payload)
item.value = token_handler(
session_id=session_id, usage_rules=usage_rules, **token_payload
)

else:
raise ValueError("Can not mint that kind of token")
Expand Down
9 changes: 6 additions & 3 deletions src/oidcop/session/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from oidcop.exception import ConfigurationError
from oidcop.token import handler
from oidcop.util import Crypt
from oidcop.session.database import NoSuchClientSession
from .database import Database
from .grant import Grant
from .grant import SessionToken
Expand Down Expand Up @@ -226,9 +227,11 @@ def create_session(
if not client_id:
client_id = auth_req["client_id"]

client_info = ClientSessionInfo(client_id=client_id)

self.set([user_id, client_id], client_info)
try:
self.get([user_id, client_id])
except (NoSuchClientSession, ValueError):
client_info = ClientSessionInfo(client_id=client_id)
self.set([user_id, client_id], client_info)

return self.create_grant(
auth_req=auth_req,
Expand Down
Loading