simple_openid_connect 2.3.2__tar.gz → 2.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/.envrc.dist +2 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/.github/workflows/checks.yml +12 -9
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/CHANGELOG.md +12 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/PKG-INFO +4 -4
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/README.md +3 -3
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/requirements.dev.txt +1 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/src/simple_openid_connect/__init__.py +1 -1
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/src/simple_openid_connect/client.py +80 -13
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/src/simple_openid_connect/integrations/django/admin.py +16 -1
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/src/simple_openid_connect/integrations/django/templates/simple_openid_connect/admin/openid_session_inline.html +6 -3
- simple_openid_connect-2.4.0/src/simple_openid_connect/jwk.py +59 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/tests/conftest.py +24 -1
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/tests/test_client.py +9 -2
- simple_openid_connect-2.4.0/tests/test_jwk.py +47 -0
- simple_openid_connect-2.3.2/src/simple_openid_connect/jwk.py +0 -23
- simple_openid_connect-2.3.2/tests/test_jwk.py +0 -18
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/.git-blame-ignore-revs +0 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/.gitattributes +0 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/.gitignore +0 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/.pre-commit-config.yaml +0 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/.readthedocs.yaml +0 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/DEVELOPMENT.md +0 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/LICENSE +0 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/dev/compose.yaml +0 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/dev/keycloak_exports/realm.json +0 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/docs/Makefile +0 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/docs/_static/.gitkeep +0 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/docs/_templates/.gitkeep +0 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/docs/api.rst +0 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/docs/conf.py +0 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/docs/django-integration.rst +0 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/docs/drf-integration.rst +0 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/docs/index.rst +0 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/docs/installation.rst +0 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/docs/nonce_and_state.rst +0 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/docs/usage.rst +0 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/flake.lock +0 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/flake.nix +0 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/pyproject.toml +0 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/src/simple_openid_connect/base_data.py +0 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/src/simple_openid_connect/client_authentication.py +0 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/src/simple_openid_connect/data.py +0 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/src/simple_openid_connect/discovery.py +0 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/src/simple_openid_connect/exceptions.py +0 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/src/simple_openid_connect/flows/__init__.py +0 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/src/simple_openid_connect/flows/authorization_code_flow/__init__.py +0 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/src/simple_openid_connect/flows/authorization_code_flow/client.py +0 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/src/simple_openid_connect/flows/client_credentials_grant/__init__.py +0 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/src/simple_openid_connect/flows/client_credentials_grant/client.py +0 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/src/simple_openid_connect/flows/direct_access_grant/__init__.py +0 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/src/simple_openid_connect/flows/direct_access_grant/client.py +0 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/src/simple_openid_connect/integrations/__init__.py +0 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/src/simple_openid_connect/integrations/django/__init__.py +0 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/src/simple_openid_connect/integrations/django/apps.py +0 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/src/simple_openid_connect/integrations/django/decorators.py +0 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/src/simple_openid_connect/integrations/django/management/__init__.py +0 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/src/simple_openid_connect/integrations/django/management/commands/__init__.py +0 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/src/simple_openid_connect/integrations/django/management/commands/clear-expired-openid-sessions.py +0 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/src/simple_openid_connect/integrations/django/middleware.py +0 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/src/simple_openid_connect/integrations/django/migrations/0001_initial.py +0 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/src/simple_openid_connect/integrations/django/migrations/0002_move_sessions_id_token.py +0 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/src/simple_openid_connect/integrations/django/migrations/0003_openidsession_add_raw_id_token.py +0 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/src/simple_openid_connect/integrations/django/migrations/__init__.py +0 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/src/simple_openid_connect/integrations/django/models.py +0 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/src/simple_openid_connect/integrations/django/static/simple_openid_connect/login_failed.css +0 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/src/simple_openid_connect/integrations/django/templates/simple_openid_connect/login_failed.html +0 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/src/simple_openid_connect/integrations/django/urls.py +0 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/src/simple_openid_connect/integrations/django/user_mapping.py +0 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/src/simple_openid_connect/integrations/django/views.py +0 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/src/simple_openid_connect/integrations/djangorestframework/__init__.py +0 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/src/simple_openid_connect/integrations/djangorestframework/authentication.py +0 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/src/simple_openid_connect/integrations/djangorestframework/drf_spectacular_schema.py +0 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/src/simple_openid_connect/integrations/djangorestframework/permissions.py +0 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/src/simple_openid_connect/pkce.py +0 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/src/simple_openid_connect/py.typed +0 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/src/simple_openid_connect/rp_initiated_logout.py +0 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/src/simple_openid_connect/scripts/__init__.py +0 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/src/simple_openid_connect/scripts/show_openid_tokens.py +0 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/src/simple_openid_connect/token_introspection.py +0 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/src/simple_openid_connect/token_refresh.py +0 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/src/simple_openid_connect/userinfo.py +0 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/src/simple_openid_connect/utils.py +0 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/tests/django_test_project/django_test_project/__init__.py +0 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/tests/django_test_project/django_test_project/settings_base.py +0 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/tests/django_test_project/django_test_project/settings_dev.py +0 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/tests/django_test_project/django_test_project/settings_test.py +0 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/tests/django_test_project/django_test_project/templates/admin/login.html +0 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/tests/django_test_project/django_test_project/tests/conftest.py +0 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/tests/django_test_project/django_test_project/tests/test_access_token_protected_view.py +0 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/tests/django_test_project/django_test_project/tests/test_frontchannel_logout_notifications.py +0 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/tests/django_test_project/django_test_project/tests/test_login.py +0 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/tests/django_test_project/django_test_project/tests/test_logout.py +0 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/tests/django_test_project/django_test_project/urls.py +0 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/tests/django_test_project/django_test_project/views.py +0 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/tests/django_test_project/django_test_project/wsgi.py +0 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/tests/django_test_project/manage.py +0 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/tests/test_authorization_code_flow.py +0 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/tests/test_client_auth.py +0 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/tests/test_client_credentials_grant.py +0 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/tests/test_direct_access_grant.py +0 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/tests/test_discovery.py +0 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/tests/test_message_encoding.py +0 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/tests/test_pkce.py +0 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/tests/test_rp_initiated_logout.py +0 -0
- {simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/tests/test_validate_extern.py +0 -0
|
@@ -27,18 +27,21 @@ jobs:
|
|
|
27
27
|
strategy:
|
|
28
28
|
fail-fast: false
|
|
29
29
|
matrix:
|
|
30
|
-
py_version: ["3.
|
|
31
|
-
drf_version: ["3.
|
|
30
|
+
py_version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
|
|
31
|
+
drf_version: ["3.15", "3.16", "3.17"]
|
|
32
32
|
django_version:
|
|
33
33
|
- "4.2" # LTS
|
|
34
|
-
- "5.
|
|
35
|
-
- "
|
|
34
|
+
- "5.2" # LTS
|
|
35
|
+
- "6" # current major version
|
|
36
36
|
exclude:
|
|
37
|
-
# django
|
|
38
|
-
- py_version: "3.
|
|
39
|
-
django_version: "
|
|
40
|
-
- py_version: "3.
|
|
41
|
-
django_version: "
|
|
37
|
+
# exclude new django versions with old python versions
|
|
38
|
+
- py_version: "3.10"
|
|
39
|
+
django_version: "6"
|
|
40
|
+
- py_version: "3.11"
|
|
41
|
+
django_version: "6"
|
|
42
|
+
# exclude old django versions with new python versions
|
|
43
|
+
- py_version: "3.14"
|
|
44
|
+
django_version: "4.2"
|
|
42
45
|
runs-on: ubuntu-latest
|
|
43
46
|
steps:
|
|
44
47
|
- uses: actions/checkout@v3
|
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v2.4.0
|
|
4
|
+
|
|
5
|
+
### Notable Features
|
|
6
|
+
|
|
7
|
+
- JSON-Web-Keys of Identity-Providers are not cached indefinitely anymore.
|
|
8
|
+
In previous versions, when using the *client-style* API of this library, any instance of `OpenidClient` would fetch JSON-Web-Keys from the configured identity provider during initialization and then keep them forever. This has been changed so that JWKs are now only fetched on demand and cached based on the Identity-Providers HTTP `Cache-Control` header information. Because many Identity-Provider implementations don't send 'Cache-Control' headers, a default minimum cache of 10 minutes has also been implemented.
|
|
9
|
+
|
|
10
|
+
### Minor Changes
|
|
11
|
+
|
|
12
|
+
- The supported list of python and integration versions has been updated to match currently supported upstream ones.
|
|
13
|
+
- The included django admin view now only displays raw tokens when djangos debug mode is enabled.
|
|
14
|
+
|
|
3
15
|
## v2.3.2
|
|
4
16
|
|
|
5
17
|
### Bug-Fixes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: simple_openid_connect
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.4.0
|
|
4
4
|
Summary: Simple and opinionated OpenID-Connect relying party and resource server implementation
|
|
5
5
|
Author-email: lilly <li@lly.sh>
|
|
6
6
|
Requires-Python: ~=3.9
|
|
@@ -66,14 +66,14 @@ Simple and opinionated OpenID-Connect relying party and resource server implemen
|
|
|
66
66
|
|
|
67
67
|
## Language & Framework Support
|
|
68
68
|
|
|
69
|
-
We aim to follow pythons officially supported versions. This is currently python `v3.
|
|
69
|
+
We aim to follow pythons officially supported versions. This is currently python `v3.10`, `v3.11`, `v3.12`, `v3.13` and `v3.14`.
|
|
70
70
|
|
|
71
71
|
In addition to being an OpenID library from which one can build their own authentication solutaion, we also offer explicit integrations into the following frameworks and projects:
|
|
72
72
|
|
|
73
73
|
| Name | Package Feature | Integration Docs | Supported Versions |
|
|
74
74
|
|-----------------------------------------------------------------|-----------------------|----------------------------------------------------------------------------------------------------|---------------------------|
|
|
75
|
-
| [Django](https://www.djangoproject.com/) | `django` | [Integration Docs](https://simple-openid-connect.readthedocs.io/en/stable/django-integration.html) | `v4.2`, `v5.
|
|
76
|
-
| [Django-Rest-Framework](https://www.django-rest-framework.org/) | `djangorestframework` | [Integration Docs](https://simple-openid-connect.readthedocs.io/en/stable/drf-integration.html) | `v3.
|
|
75
|
+
| [Django](https://www.djangoproject.com/) | `django` | [Integration Docs](https://simple-openid-connect.readthedocs.io/en/stable/django-integration.html) | `v4.2`, `v5.2`, `v6.0` |
|
|
76
|
+
| [Django-Rest-Framework](https://www.django-rest-framework.org/) | `djangorestframework` | [Integration Docs](https://simple-openid-connect.readthedocs.io/en/stable/drf-integration.html) | `v3.15`, `v3.16`, `v3.17` |
|
|
77
77
|
|
|
78
78
|
|
|
79
79
|
|
|
@@ -33,14 +33,14 @@ Simple and opinionated OpenID-Connect relying party and resource server implemen
|
|
|
33
33
|
|
|
34
34
|
## Language & Framework Support
|
|
35
35
|
|
|
36
|
-
We aim to follow pythons officially supported versions. This is currently python `v3.
|
|
36
|
+
We aim to follow pythons officially supported versions. This is currently python `v3.10`, `v3.11`, `v3.12`, `v3.13` and `v3.14`.
|
|
37
37
|
|
|
38
38
|
In addition to being an OpenID library from which one can build their own authentication solutaion, we also offer explicit integrations into the following frameworks and projects:
|
|
39
39
|
|
|
40
40
|
| Name | Package Feature | Integration Docs | Supported Versions |
|
|
41
41
|
|-----------------------------------------------------------------|-----------------------|----------------------------------------------------------------------------------------------------|---------------------------|
|
|
42
|
-
| [Django](https://www.djangoproject.com/) | `django` | [Integration Docs](https://simple-openid-connect.readthedocs.io/en/stable/django-integration.html) | `v4.2`, `v5.
|
|
43
|
-
| [Django-Rest-Framework](https://www.django-rest-framework.org/) | `djangorestframework` | [Integration Docs](https://simple-openid-connect.readthedocs.io/en/stable/drf-integration.html) | `v3.
|
|
42
|
+
| [Django](https://www.djangoproject.com/) | `django` | [Integration Docs](https://simple-openid-connect.readthedocs.io/en/stable/django-integration.html) | `v4.2`, `v5.2`, `v6.0` |
|
|
43
|
+
| [Django-Rest-Framework](https://www.django-rest-framework.org/) | `djangorestframework` | [Integration Docs](https://simple-openid-connect.readthedocs.io/en/stable/drf-integration.html) | `v3.15`, `v3.16`, `v3.17` |
|
|
44
44
|
|
|
45
45
|
|
|
46
46
|
|
{simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/src/simple_openid_connect/client.py
RENAMED
|
@@ -14,10 +14,11 @@ from typing import (
|
|
|
14
14
|
TypeVar,
|
|
15
15
|
Union,
|
|
16
16
|
)
|
|
17
|
+
from datetime import datetime, timedelta, timezone
|
|
18
|
+
import warnings
|
|
17
19
|
|
|
18
20
|
from cryptojwt import JWK
|
|
19
21
|
from cryptojwt.jwk.jwk import key_from_jwk_dict
|
|
20
|
-
|
|
21
22
|
from simple_openid_connect import (
|
|
22
23
|
jwk,
|
|
23
24
|
rp_initiated_logout,
|
|
@@ -62,8 +63,8 @@ class OpenidClient:
|
|
|
62
63
|
"""
|
|
63
64
|
|
|
64
65
|
provider_config: ProviderMetadata
|
|
65
|
-
provider_keys: List[JWK]
|
|
66
66
|
client_auth: ClientAuthenticationMethod
|
|
67
|
+
min_jwks_cache_duration: timedelta
|
|
67
68
|
scope: str
|
|
68
69
|
|
|
69
70
|
authorization_code_flow: AuthorizationCodeFlowClient
|
|
@@ -75,22 +76,48 @@ class OpenidClient:
|
|
|
75
76
|
client_credentials_grant: ClientCredentialsGrantClient
|
|
76
77
|
"*Client Credentials Grant* (or *Service Account Authentication*) functionality"
|
|
77
78
|
|
|
79
|
+
_jwks_max_age: Optional[datetime]
|
|
80
|
+
"Maximum time until the currently cached provider keys are valid"
|
|
81
|
+
_provider_keys: Optional[List[JWK]]
|
|
82
|
+
"Cached provider keys that can be used until they expire"
|
|
83
|
+
|
|
78
84
|
def __init__(
|
|
79
85
|
self,
|
|
80
86
|
provider_config: ProviderMetadata,
|
|
81
|
-
provider_keys: List[JWK],
|
|
82
|
-
authentication_redirect_uri: Optional[str],
|
|
83
|
-
client_id: str,
|
|
87
|
+
provider_keys: Optional[List[JWK]] = None,
|
|
88
|
+
authentication_redirect_uri: Optional[str] = None,
|
|
89
|
+
client_id: str = "",
|
|
84
90
|
client_secret: Optional[str] = None,
|
|
85
91
|
scope: str = "openid",
|
|
92
|
+
min_jwks_cache_duration: timedelta = timedelta(minutes=10),
|
|
86
93
|
):
|
|
94
|
+
"""
|
|
95
|
+
Construct a new client that is bound to an Identity-Provider.
|
|
96
|
+
|
|
97
|
+
See :func:`from_issuer_url` for a more convenient factory that constructs a client instance.
|
|
98
|
+
|
|
99
|
+
:param provider_config: Configuration of the identity provider, probably fetched via autodiscovery
|
|
100
|
+
:param authentication_redirect_uri: The URL that a User-Agent should be redirected to during the authentication process to reach this client
|
|
101
|
+
:param client_id: The client_id for this client. This is assigned to you by the Identity-Provider
|
|
102
|
+
:param client_secret: The client_secret for this client. This is assigned to you by the Identity-Provider and is recommended to set. If this client is not able to keep it's secret safe, it shoud however not be set.
|
|
103
|
+
:param scope: The authorization scope this client should request from the Identity-Provider
|
|
104
|
+
:param min_jwks_cache_duration: Minimum time this client will cache JWKs responses from the Identity-Provider. If set to 0, the Identity-Providers Cache-Control HTTP headers will be resepcted fully.
|
|
105
|
+
"""
|
|
87
106
|
self.provider_config = provider_config
|
|
88
|
-
self.
|
|
107
|
+
self.scope = scope
|
|
108
|
+
self.authentication_redirect_uri = authentication_redirect_uri
|
|
109
|
+
self.min_jwks_cache_duration = min_jwks_cache_duration
|
|
89
110
|
self.authorization_code_flow = AuthorizationCodeFlowClient(self)
|
|
90
111
|
self.direct_access_grant = DirectAccessGrantClient(self)
|
|
91
112
|
self.client_credentials_grant = ClientCredentialsGrantClient(self)
|
|
92
|
-
self.
|
|
93
|
-
self.
|
|
113
|
+
self._jwks_max_age = None
|
|
114
|
+
self._provider_keys = None
|
|
115
|
+
|
|
116
|
+
if provider_keys is not None:
|
|
117
|
+
warnings.warn(
|
|
118
|
+
"giving a provider_keys= parameter to the OpenidClient constructor and the parameter will be removed in a future release",
|
|
119
|
+
DeprecationWarning,
|
|
120
|
+
)
|
|
94
121
|
|
|
95
122
|
if client_secret is None:
|
|
96
123
|
self.client_auth = NoneAuth(client_id)
|
|
@@ -113,6 +140,7 @@ class OpenidClient:
|
|
|
113
140
|
client_id: str,
|
|
114
141
|
client_secret: Union[str, None] = None,
|
|
115
142
|
scope: str = "openid",
|
|
143
|
+
min_jwks_cache_duration: timedelta = timedelta(minutes=10),
|
|
116
144
|
) -> Self:
|
|
117
145
|
"""
|
|
118
146
|
Create a new client instance with an issuer url as base, automatically discovering information about the issuer in the process.
|
|
@@ -124,11 +152,17 @@ class OpenidClient:
|
|
|
124
152
|
:param client_secret: Optionally a client secret which has been assigned to your client from the issuer.
|
|
125
153
|
If not supplied, this client is assumed to be *public* which means it has not client secret because it cannot be kept safe (e.g. a web-app).
|
|
126
154
|
:param scope: Which scopes to request from the OP
|
|
155
|
+
:param min_jwks_cache_duration: Minimum time this client will cache JWKs responses from the Identity-Provider. If set to 0, the Identity-Providers Cache-Control HTTP headers will be resepcted fully.
|
|
127
156
|
"""
|
|
128
157
|
|
|
129
158
|
config = discover_configuration_from_issuer(url)
|
|
130
159
|
return cls.from_issuer_config(
|
|
131
|
-
config,
|
|
160
|
+
config,
|
|
161
|
+
authentication_redirect_uri,
|
|
162
|
+
client_id,
|
|
163
|
+
client_secret,
|
|
164
|
+
scope,
|
|
165
|
+
min_jwks_cache_duration,
|
|
132
166
|
)
|
|
133
167
|
|
|
134
168
|
@classmethod
|
|
@@ -139,6 +173,7 @@ class OpenidClient:
|
|
|
139
173
|
client_id: str,
|
|
140
174
|
client_secret: Union[str, None] = None,
|
|
141
175
|
scope: str = "openid",
|
|
176
|
+
min_jwks_cache_duration: timedelta = timedelta(minutes=10),
|
|
142
177
|
) -> Self:
|
|
143
178
|
"""
|
|
144
179
|
Create a new client instance with a resolved issuer configuration as base.
|
|
@@ -152,12 +187,40 @@ class OpenidClient:
|
|
|
152
187
|
:param client_secret: Optionally a client secret which has been assigned to your client from the issuer.
|
|
153
188
|
If not supplied, this client is assumed to be *public* which means it has not client secret because it cannot be kept safe (e.g. a web-app).
|
|
154
189
|
:param scope: Which scopes to request from the OP
|
|
190
|
+
:param min_jwks_cache_duration: Minimum time this client will cache JWKs responses from the Identity-Provider. If set to 0, the Identity-Providers Cache-Control HTTP headers will be resepcted fully.
|
|
155
191
|
"""
|
|
156
|
-
keys = jwk.fetch_jwks(config.jwks_uri)
|
|
157
192
|
return cls(
|
|
158
|
-
config,
|
|
193
|
+
config,
|
|
194
|
+
None,
|
|
195
|
+
authentication_redirect_uri,
|
|
196
|
+
client_id,
|
|
197
|
+
client_secret,
|
|
198
|
+
scope,
|
|
199
|
+
min_jwks_cache_duration,
|
|
159
200
|
)
|
|
160
201
|
|
|
202
|
+
@property
|
|
203
|
+
def provider_keys(self) -> List[JWK]:
|
|
204
|
+
now = datetime.now(timezone.utc)
|
|
205
|
+
|
|
206
|
+
# return the cached data if it is still valid
|
|
207
|
+
if (
|
|
208
|
+
self._provider_keys is not None
|
|
209
|
+
and self._jwks_max_age is not None
|
|
210
|
+
and now <= self._jwks_max_age
|
|
211
|
+
):
|
|
212
|
+
return self._provider_keys
|
|
213
|
+
|
|
214
|
+
# fetch new keys otherwise
|
|
215
|
+
jwks, idp_max_age = jwk.fetch_jwks_max_age(self.provider_config.jwks_uri)
|
|
216
|
+
self._provider_keys = jwks
|
|
217
|
+
|
|
218
|
+
# remember how long we can cache the retrieved keys
|
|
219
|
+
# if the idp did not return any max-age information, interpret as expiring immediately but also cache for the minimum configured time
|
|
220
|
+
self._jwks_max_age = max(idp_max_age or now, now + self.min_jwks_cache_duration)
|
|
221
|
+
|
|
222
|
+
return self._provider_keys
|
|
223
|
+
|
|
161
224
|
@property
|
|
162
225
|
def client_type(self) -> Literal["public", "confidential"]:
|
|
163
226
|
"""
|
|
@@ -309,11 +372,15 @@ class OpenidClient:
|
|
|
309
372
|
# this implements support for pickling this class
|
|
310
373
|
# it is basically the default pickle behavior but explicitly serializes keys because they are FFI backed and not normally picklable
|
|
311
374
|
result = self.__dict__.copy()
|
|
312
|
-
|
|
375
|
+
if self._provider_keys is not None:
|
|
376
|
+
result["_provider_keys"] = [k.serialize() for k in self._provider_keys]
|
|
313
377
|
return result
|
|
314
378
|
|
|
315
379
|
def __setstate__(self, state: Dict[str, Any]) -> None:
|
|
316
380
|
# this implements support for unpickling this class
|
|
317
381
|
# it is basically the default pickle behavior but explicitly deserializes keys
|
|
318
|
-
state["
|
|
382
|
+
if state["_provider_keys"] is not None:
|
|
383
|
+
state["_provider_keys"] = [
|
|
384
|
+
key_from_jwk_dict(k) for k in state["_provider_keys"]
|
|
385
|
+
]
|
|
319
386
|
self.__dict__ = state
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
from typing import List
|
|
1
|
+
from typing import List, Any, Dict, Optional
|
|
2
2
|
|
|
3
3
|
from django.contrib import admin
|
|
4
4
|
from django.http import HttpRequest
|
|
5
5
|
from django.contrib.auth import get_user_model
|
|
6
|
+
from django.conf import settings
|
|
6
7
|
|
|
7
8
|
from . import models
|
|
8
9
|
|
|
@@ -34,3 +35,17 @@ class OpenidUserAdmin(admin.ModelAdmin): # type: ignore[type-arg]
|
|
|
34
35
|
result.append(f"user__{UserModel.EMAIL_FIELD}")
|
|
35
36
|
|
|
36
37
|
return result
|
|
38
|
+
|
|
39
|
+
def change_view(
|
|
40
|
+
self,
|
|
41
|
+
request: Any,
|
|
42
|
+
object_id: Any,
|
|
43
|
+
form_url: Any = "",
|
|
44
|
+
extra_context: Optional[Dict[str, Any]] = None,
|
|
45
|
+
) -> Any:
|
|
46
|
+
# add a settings.DEBUG context variable
|
|
47
|
+
extra_context = extra_context or dict()
|
|
48
|
+
extra_context.setdefault("settings", dict())
|
|
49
|
+
extra_context["settings"]["DEBUG"] = settings.DEBUG and True
|
|
50
|
+
|
|
51
|
+
return super().change_view(request, object_id, form_url, extra_context)
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
<fieldset class="module aligned" aria-labelledby="{{ inline_admin_formset.formset.prefix }}-heading">
|
|
2
2
|
<h2 id="{{ inline_admin_formset.formset.prefix }}-heading" class="inline-heading">OpenID Sessions</h2>
|
|
3
3
|
|
|
4
|
-
|
|
5
4
|
{% for inline_admin_form in inline_admin_formset %}
|
|
6
5
|
{% with i_session=inline_admin_form.original %}
|
|
7
6
|
{% if i_session %}
|
|
@@ -27,12 +26,14 @@
|
|
|
27
26
|
<span style="color: green;">Valid until {{ i_session.access_token_expiry | default:"-empty-" }}</span>
|
|
28
27
|
{% endif %}
|
|
29
28
|
</div>
|
|
29
|
+
{% if settings.DEBUG %}
|
|
30
30
|
<div style="margin-left: 2rem; min-width: 128px; padding: 6px 0;">
|
|
31
31
|
<details>
|
|
32
32
|
<summary>Token</summary>
|
|
33
33
|
<code style="display: block; padding: 8px; line-break: anywhere;">{{ i_session.access_token }}</code>
|
|
34
|
-
|
|
34
|
+
</details>
|
|
35
35
|
</div>
|
|
36
|
+
{% endif %}
|
|
36
37
|
</div>
|
|
37
38
|
|
|
38
39
|
<div class="flex-container">
|
|
@@ -44,12 +45,14 @@
|
|
|
44
45
|
<span style="color: green;">Valid until {{ i_session.refresh_token_expiry | default:"-empty-" }}</span>
|
|
45
46
|
{% endif %}
|
|
46
47
|
</div>
|
|
48
|
+
{% if settings.DEBUG %}
|
|
47
49
|
<div style="margin-left: 2rem; min-width: 128px; padding: 6px 0;">
|
|
48
50
|
<details>
|
|
49
51
|
<summary>Token</summary>
|
|
50
52
|
<code style="display: block; padding: 8px; line-break: anywhere;">{{ i_session.refresh_token }}</code>
|
|
51
|
-
|
|
53
|
+
</details>
|
|
52
54
|
</div>
|
|
55
|
+
{% endif %}
|
|
53
56
|
</div>
|
|
54
57
|
|
|
55
58
|
<div class="flex-container">
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""
|
|
2
|
+
JSON-Web-Key handling code
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import List, cast, Tuple, Any, Optional
|
|
6
|
+
from datetime import datetime, timedelta, timezone
|
|
7
|
+
|
|
8
|
+
from cryptojwt import JWK, KeyBundle
|
|
9
|
+
|
|
10
|
+
from simple_openid_connect.exceptions import OpenidProtocolError
|
|
11
|
+
import requests
|
|
12
|
+
|
|
13
|
+
requests.request
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class _Httpc:
|
|
17
|
+
"""
|
|
18
|
+
A wrapper around ``requests.request`` which assigns a max_age parameter as instance variable for later retrieval
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
max_age: Optional[datetime] = None
|
|
22
|
+
|
|
23
|
+
def __call__(self, *args: Any, **kwargs: Any) -> requests.Response:
|
|
24
|
+
now = datetime.now(timezone.utc)
|
|
25
|
+
response = requests.request(*args, **kwargs)
|
|
26
|
+
|
|
27
|
+
# extract Cache-Control header and save its value for later
|
|
28
|
+
cache_control = response.headers.get("Cache-Control")
|
|
29
|
+
if cache_control and "max-age" in cache_control:
|
|
30
|
+
http_max_age = int(cache_control.split("max-age=")[1].split(",")[0])
|
|
31
|
+
self.max_age = now + timedelta(seconds=http_max_age)
|
|
32
|
+
|
|
33
|
+
return response
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def fetch_jwks_max_age(jwks_uri: str) -> Tuple[List[JWK], Optional[datetime]]:
|
|
37
|
+
"""
|
|
38
|
+
Fetch JSON web keys from the given jwks_uri.
|
|
39
|
+
This uri is part of the provider configuration and used to validate responses and tokens sent by the provider.
|
|
40
|
+
|
|
41
|
+
:raises OpenidProtocolError: If fetching the keys fails
|
|
42
|
+
"""
|
|
43
|
+
httpc = _Httpc()
|
|
44
|
+
bundle = KeyBundle(source=jwks_uri, httpc=httpc)
|
|
45
|
+
keys = cast(List[JWK], bundle.keys())
|
|
46
|
+
if not keys:
|
|
47
|
+
raise OpenidProtocolError("Failed to fetch keys")
|
|
48
|
+
return keys, httpc.max_age
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def fetch_jwks(jwks_uri: str) -> List[JWK]:
|
|
52
|
+
"""
|
|
53
|
+
Fetch JSON web keys from the given jwks_uri.
|
|
54
|
+
This uri is part of the provider configuration and used to validate responses and tokens sent by the provider.
|
|
55
|
+
|
|
56
|
+
:raises OpenidProtocolError: If fetching the keys fails
|
|
57
|
+
"""
|
|
58
|
+
keys, _ = fetch_jwks_max_age(jwks_uri)
|
|
59
|
+
return keys
|
|
@@ -65,7 +65,7 @@ def known_provider_configs(response_mock):
|
|
|
65
65
|
"""
|
|
66
66
|
Mock requests to known OpenID provider config URLs to return static content
|
|
67
67
|
"""
|
|
68
|
-
# mafiasi identity
|
|
68
|
+
# mafiasi identity (keycloak)
|
|
69
69
|
response_mock.get(
|
|
70
70
|
url="https://identity.mafiasi.de/auth/realms/simple_openid_test/.well-known/openid-configuration",
|
|
71
71
|
content_type="application/json",
|
|
@@ -77,6 +77,18 @@ def known_provider_configs(response_mock):
|
|
|
77
77
|
body='{"keys":[{"kid":"JAehgk0O0uzTEht7KGCPVB_urwfsBGe22phHVDZezeo","kty":"RSA","alg":"RS256","use":"sig","n":"n1t4R6mVlBZggtmEM5HohZYg5QjLtjlRVyAMMM93H4WRPpP2Xqj3RrGwP-LZr6lQhk6w8nvPNGnFujWtXb_dBrhZDBh2z0mCNvfjJMp5Ki5sLT9nCbuZ4NkIhfx7qTzQ--GomQ6uKOSuoE12A_r9UopwuCf_1zrx0MIqN_kMeYU2px5yJW-5qbwPL9hcWcfY_Pdz5SFcQ3kDi6GlXA0PN74Ivhs9baVIv60cwB0TytQXGn__GYFZ-K5qAjIymyIy8cPRy9N27Gs29cWF-C56n1pAxwkcPY1SAQgcrspIhntQbZBeGboUx2pnQ_v8LEqVt9ahh52eAbV36YoQqu1DbQ","e":"AQAB","x5c":["MIICszCCAZsCBgGEZpqrijANBgkqhkiG9w0BAQsFADAdMRswGQYDVQQDDBJzaW1wbGVfb3BlbmlkX3Rlc3QwHhcNMjIxMTExMTIxMDIzWhcNMzIxMTExMTIxMjAzWjAdMRswGQYDVQQDDBJzaW1wbGVfb3BlbmlkX3Rlc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCfW3hHqZWUFmCC2YQzkeiFliDlCMu2OVFXIAwwz3cfhZE+k/ZeqPdGsbA/4tmvqVCGTrDye880acW6Na1dv90GuFkMGHbPSYI29+MkynkqLmwtP2cJu5ng2QiF/HupPND74aiZDq4o5K6gTXYD+v1SinC4J//XOvHQwio3+Qx5hTanHnIlb7mpvA8v2FxZx9j893PlIVxDeQOLoaVcDQ83vgi+Gz1tpUi/rRzAHRPK1Bcaf/8ZgVn4rmoCMjKbIjLxw9HL03bsazb1xYX4LnqfWkDHCRw9jVIBCByuykiGe1BtkF4ZuhTHamdD+/wsSpW31qGHnZ4BtXfpihCq7UNtAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAD3aKq2xiLNPlUcmxwtR1FihkEM+NM7VV+m+74Lf9m/lhNzuM00JZAW67gIz2kO5K4XaU84PCRzt3sELma/xVoBs3iW8AhtldTax17g0NZzpMEExzYi0vyihq8xihbx+8XrVwxQ3JBkb/y3b+otDX3xRxBKSCDuxuigL1bX7UzgJ3oe4wD6m8yN7nrMdqSMbspd8lkceKNPziclXnbKPIUwrnxdWMmB41zYcT3y+zHxqk6EDkf3NU+C3gCoRCQoBKcK4X2dwZdUbv0ginU9kROjNXc2+gP53CYDT4fqOEX/jBuljbYb9ziaQ9/5kJZ9SsazgWNjpf4VvhAsH64i7+dE="],"x5t":"PxLqu4M4uNWaUhbfkvI0tGrkhtQ","x5t#S256":"OWVSYSSNFaLyfxV0Lg6XyVDnB2t6sHwtNbzVB2fNCVc"}]}',
|
|
78
78
|
)
|
|
79
79
|
|
|
80
|
+
# hanse.de (authentik)
|
|
81
|
+
response_mock.get(
|
|
82
|
+
url="https://auth.hanse.de/application/o/git/.well-known/openid-configuration",
|
|
83
|
+
content_type="application/json",
|
|
84
|
+
body='{"issuer":"https://auth.hanse.de/application/o/git/","authorization_endpoint":"https://auth.hanse.de/application/o/authorize/","token_endpoint":"https://auth.hanse.de/application/o/token/","userinfo_endpoint":"https://auth.hanse.de/application/o/userinfo/","end_session_endpoint":"https://auth.hanse.de/application/o/git/end-session/","introspection_endpoint":"https://auth.hanse.de/application/o/introspect/","revocation_endpoint":"https://auth.hanse.de/application/o/revoke/","device_authorization_endpoint":"https://auth.hanse.de/application/o/device/","backchannel_logout_supported":true,"backchannel_logout_session_supported":true,"response_types_supported":["code","id_token","id_token token","code token","code id_token","code id_token token"],"response_modes_supported":["query","fragment","form_post"],"jwks_uri":"https://auth.hanse.de/application/o/git/jwks/","grant_types_supported":["authorization_code","refresh_token","implicit","client_credentials","password","urn:ietf:params:oauth:grant-type:device_code"],"id_token_signing_alg_values_supported":["RS256"],"subject_types_supported":["public"],"token_endpoint_auth_methods_supported":["client_secret_post","client_secret_basic"],"acr_values_supported":["goauthentik.io/providers/oauth2/default"],"scopes_supported":["email","profile","offline_access","openid"],"request_parameter_supported":false,"claims_supported":["sub","iss","aud","exp","iat","auth_time","acr","amr","nonce","email","email_verified","name","given_name","preferred_username","nickname","groups"],"claims_parameter_supported":false,"code_challenge_methods_supported":["plain","S256"]}',
|
|
85
|
+
)
|
|
86
|
+
response_mock.get(
|
|
87
|
+
url="https://auth.hanse.de/application/o/git/jwks/",
|
|
88
|
+
content_type="application/json",
|
|
89
|
+
body='{"keys":[{"alg":"RS256","kid":"25bfaaaa8e5aed3a44d130d66ac81022","kty":"RSA","use":"sig","n":"tpOexiJZfPHxl3GOiQJCCmkM9GVkG7R-DUYwUYTuTDnGR0XClAD3HRWe7yW-Lzurk8kBH6OjfLU3aYhQAhjWBblvmTSXcIDRRkckHwYj-M4b59wxZW26rdSuI0FzUrJOEOMpNtm_HzxvdUyWYQQXYl7P9BNuJtTT0AG3Od-VC7hZMSLU_42cRxo3XTzbqpFfJlF1TRtjDh7_RB8oofohGF2LRKB-iP3wrRFO2_erLJI5rbxPUBiVhDisPau0R9kvIC-BbcL_RBHfZyZgl78PkWEDaRXCZ8JtweHnoMWZw4-6sIQHDcbigz6UFVlngcaV3aktPNobLZyJcAR4K_Xfz_FA135HOXpqzsS064TuksOj8TnklMHoz4k-Y7Q8FZFvFgJw_R4fxOG23IMfzWHmAEt8qhrcLfd_kZCP2dUbmH_z984NZ0Y34fZRSP7m5CMxGFzOFpueSjCBWRpgzifpEP4LBVZ1PyP5a06xHf_87imXdZ6oYgBcGBVmmsM9lSJhR4-XYuyz6sHvIXnMSRMw7LG6hqfh2fKUnRQIzcP_ldU6nQdTzA0cRcsYJ7HnoWCJj78uPo5vLVTjz3eoQuk8z5Beyce43SdkUH1kHbaobI8uxuwOcMnQVnRge3YFIpZR3SO5UPjSyjNhDFfuz2jt5idQT9cKX1vLFo6BtUf_1J8","e":"AQAB","x5c":["MIIFUzCCAzugAwIBAgIQX/aqWMV4Rf27MyUo/KcDYDANBgkqhkiG9w0BAQsFADAeMRwwGgYDVQQDDBNhdXRoZW50aWsgMjAyNS4xMC4wMB4XDTI1MTAyOTIzMjQ0M1oXDTI2MTAzMDIzMjQ0M1owVjEqMCgGA1UEAwwhYXV0aGVudGlrIFNlbGYtc2lnbmVkIENlcnRpZmljYXRlMRIwEAYDVQQKDAlhdXRoZW50aWsxFDASBgNVBAsMC1NlbGYtc2lnbmVkMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAtpOexiJZfPHxl3GOiQJCCmkM9GVkG7R+DUYwUYTuTDnGR0XClAD3HRWe7yW+Lzurk8kBH6OjfLU3aYhQAhjWBblvmTSXcIDRRkckHwYj+M4b59wxZW26rdSuI0FzUrJOEOMpNtm/HzxvdUyWYQQXYl7P9BNuJtTT0AG3Od+VC7hZMSLU/42cRxo3XTzbqpFfJlF1TRtjDh7/RB8oofohGF2LRKB+iP3wrRFO2/erLJI5rbxPUBiVhDisPau0R9kvIC+BbcL/RBHfZyZgl78PkWEDaRXCZ8JtweHnoMWZw4+6sIQHDcbigz6UFVlngcaV3aktPNobLZyJcAR4K/Xfz/FA135HOXpqzsS064TuksOj8TnklMHoz4k+Y7Q8FZFvFgJw/R4fxOG23IMfzWHmAEt8qhrcLfd/kZCP2dUbmH/z984NZ0Y34fZRSP7m5CMxGFzOFpueSjCBWRpgzifpEP4LBVZ1PyP5a06xHf/87imXdZ6oYgBcGBVmmsM9lSJhR4+XYuyz6sHvIXnMSRMw7LG6hqfh2fKUnRQIzcP/ldU6nQdTzA0cRcsYJ7HnoWCJj78uPo5vLVTjz3eoQuk8z5Beyce43SdkUH1kHbaobI8uxuwOcMnQVnRge3YFIpZR3SO5UPjSyjNhDFfuz2jt5idQT9cKX1vLFo6BtUf/1J8CAwEAAaNVMFMwUQYDVR0RAQH/BEcwRYJDUm5RNjFyUnZGUm1rU1d2b2tuVWFHMVlmWHIyQ0xzcEt6Mmw4UVdodC5zZWxmLXNpZ25lZC5nb2F1dGhlbnRpay5pbzANBgkqhkiG9w0BAQsFAAOCAgEAEa+ERlfh5DxgWtGa0U2WA7E+TcoF12ZsXfO0NOIMPZ/o9Pqou1Huc21Azzyh0dMAbJEuFVFmnqNLwqvjB6iK25Gk3pJakaN9iYvzhmAsb9eYLfGmOD6Bk9fHO6chEgVCxNtPja0ROV8SaEA/r8xT7pfpPJBnbjxgQjg1UDeszv8FXij0oct9tojkDPnMWJBv0m4SfZL33Vm093P9aEGLivfi/fAbjoA1nnOF+KOyoXGMN28qXUnYuc7ekjwaCAKIfkG/b06oaH6+0qV9Fl/58HMegoqPuJTEDrueM5hNKoLDhcb/unXyko1Cd/BRIID4uvmDhZl6P3HWo/lV9qW1lZwl2Dc63Hmq0XD1Vp6m8eBdT+yjXphwcsTadfVsKnVRbc+OZ27cMY2b/FHZ6kHE/qUUk+y11BrcVFx5uzF3gkCy5JmXnzB2249fJ/ixajRGyNZVt1rt8ItYwyRUpkzaSvhsl+1N+beRVm4rUFSNbOSCNq3/4NWmNhv+kvE9bS9w/vXZpkhlKRBUgCg8hZBdVSdloO4aJPCwKOgf/C2F7KtIqmj3vIxmQ9d6yD1OXGExtLHtBlE1g5n7N1At85YLFpeJAGqMwpZH430YKAoDuVf/+C5uSzLiAsubXW8d5gt7FTemJopu8/mmLswwC3eOuCRlIg49X76SOKOjRTJVRXA="],"x5t":"B6EFPg14J2vLHlggZg2c_4d5uxE","x5t#S256":"9FYLuDimrcpGF9RbZw7JNWZzoMElCaM7Yg8J7yf_FtI"}]}',
|
|
90
|
+
)
|
|
91
|
+
|
|
80
92
|
# google
|
|
81
93
|
response_mock.get(
|
|
82
94
|
"https://accounts.google.com/.well-known/openid-configuration",
|
|
@@ -102,12 +114,23 @@ def dummy_provider_config(jwks, response_mock):
|
|
|
102
114
|
id_token_signing_alg_values_supported=["RS256"],
|
|
103
115
|
).model_dump(exclude_defaults=True),
|
|
104
116
|
)
|
|
117
|
+
|
|
105
118
|
response_mock.get(
|
|
106
119
|
url="https://provider.example.com/jwks",
|
|
107
120
|
body=jwks.jwks(),
|
|
108
121
|
content_type="application/json",
|
|
109
122
|
)
|
|
110
123
|
|
|
124
|
+
response_mock.get(
|
|
125
|
+
url="https://provider.example.com/jwks-with-expiry",
|
|
126
|
+
body=jwks.jwks(),
|
|
127
|
+
content_type="application/json",
|
|
128
|
+
headers={
|
|
129
|
+
# 60 second expiry with some other directives around that need to be stripped out
|
|
130
|
+
"Cache-Control": "no-transform, max-age=60, public",
|
|
131
|
+
},
|
|
132
|
+
)
|
|
133
|
+
|
|
111
134
|
|
|
112
135
|
@pytest.fixture
|
|
113
136
|
def dummy_auth_response(response_mock):
|
|
@@ -102,9 +102,16 @@ def test_token_introspection(dummy_provider_config, dummy_token_introspection_re
|
|
|
102
102
|
|
|
103
103
|
|
|
104
104
|
def test_pickling(dummy_provider_config):
|
|
105
|
-
# arrange
|
|
105
|
+
# arrange (make client and ensure provider JWKs is fetched)
|
|
106
106
|
client = make_client()
|
|
107
|
+
_ = client.provider_keys
|
|
108
|
+
assert client._provider_keys is not None
|
|
107
109
|
|
|
108
110
|
# act (assert not throwing)
|
|
109
111
|
enc = pickle.dumps(client)
|
|
110
|
-
|
|
112
|
+
client2 = pickle.loads(enc)
|
|
113
|
+
|
|
114
|
+
# assert provider key equality
|
|
115
|
+
assert client._provider_keys is not None
|
|
116
|
+
assert client2._provider_keys is not None
|
|
117
|
+
assert client._provider_keys == client2._provider_keys
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from datetime import datetime, timezone
|
|
2
|
+
|
|
3
|
+
from freezegun import freeze_time
|
|
4
|
+
from cryptojwt import JWK
|
|
5
|
+
|
|
6
|
+
from simple_openid_connect.jwk import fetch_jwks, fetch_jwks_max_age
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_load_mafiasi_jwks(known_provider_configs):
|
|
10
|
+
keys = fetch_jwks(
|
|
11
|
+
"https://identity.mafiasi.de/auth/realms/simple_openid_test/protocol/openid-connect/certs"
|
|
12
|
+
)
|
|
13
|
+
assert all(isinstance(k, JWK) for k in keys)
|
|
14
|
+
assert len(keys) == 1
|
|
15
|
+
assert keys[0].kid == "JAehgk0O0uzTEht7KGCPVB_urwfsBGe22phHVDZezeo"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_load_hanse_jwks(known_provider_configs):
|
|
19
|
+
keys = fetch_jwks("https://auth.hanse.de/application/o/git/jwks/")
|
|
20
|
+
assert all(isinstance(k, JWK) for k in keys)
|
|
21
|
+
assert len(keys) == 1
|
|
22
|
+
assert keys[0].kid == "25bfaaaa8e5aed3a44d130d66ac81022"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_load_dummy_jwks(dummy_provider_config):
|
|
26
|
+
keys = fetch_jwks("https://provider.example.com/jwks")
|
|
27
|
+
assert all(isinstance(k, JWK) for k in keys)
|
|
28
|
+
assert len(keys) == 1
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_load_dummy_jwks_max_age_without_cache_header(dummy_provider_config):
|
|
32
|
+
keys, max_age = fetch_jwks_max_age("https://provider.example.com/jwks")
|
|
33
|
+
assert all(isinstance(k, JWK) for k in keys)
|
|
34
|
+
assert len(keys) == 1
|
|
35
|
+
assert max_age is None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@freeze_time("2026-01-01T00:00:00")
|
|
39
|
+
def test_load_dummy_jwks_max_age_with_cache_header(dummy_provider_config):
|
|
40
|
+
keys, max_age = fetch_jwks_max_age(
|
|
41
|
+
"https://provider.example.com/jwks-with-expiry"
|
|
42
|
+
) # returns a 60 second Cache-Control header
|
|
43
|
+
assert all(isinstance(k, JWK) for k in keys)
|
|
44
|
+
assert len(keys) == 1
|
|
45
|
+
assert max_age == datetime(
|
|
46
|
+
year=2026, month=1, day=1, hour=0, minute=1, second=0, tzinfo=timezone.utc
|
|
47
|
+
)
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
JSON-Web-Key handling code
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
from typing import List, cast
|
|
6
|
-
|
|
7
|
-
from cryptojwt import JWK, KeyBundle
|
|
8
|
-
|
|
9
|
-
from simple_openid_connect.exceptions import OpenidProtocolError
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def fetch_jwks(jwks_uri: str) -> List[JWK]:
|
|
13
|
-
"""
|
|
14
|
-
Fetch JSON web keys from the given jwks_uri.
|
|
15
|
-
This uri is part of the provider configuration and used to validate responses and tokens sent by the provider.
|
|
16
|
-
|
|
17
|
-
:raises OpenidProtocolError: If fetching the keys fails
|
|
18
|
-
"""
|
|
19
|
-
bundle = KeyBundle(source=jwks_uri)
|
|
20
|
-
keys = cast(List[JWK], bundle.keys())
|
|
21
|
-
if not keys:
|
|
22
|
-
raise OpenidProtocolError("Failed to fetch keys")
|
|
23
|
-
return keys
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
from cryptojwt import JWK
|
|
2
|
-
|
|
3
|
-
from simple_openid_connect.jwk import fetch_jwks
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
def test_load_mafiasi_jwks(known_provider_configs):
|
|
7
|
-
keys = fetch_jwks(
|
|
8
|
-
"https://identity.mafiasi.de/auth/realms/simple_openid_test/protocol/openid-connect/certs"
|
|
9
|
-
)
|
|
10
|
-
assert all(isinstance(k, JWK) for k in keys)
|
|
11
|
-
assert len(keys) == 1
|
|
12
|
-
assert keys[0].kid == "JAehgk0O0uzTEht7KGCPVB_urwfsBGe22phHVDZezeo"
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def test_load_dummy_jwks(dummy_provider_config):
|
|
16
|
-
keys = fetch_jwks("https://provider.example.com/jwks")
|
|
17
|
-
assert all(isinstance(k, JWK) for k in keys)
|
|
18
|
-
assert len(keys) == 1
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/src/simple_openid_connect/base_data.py
RENAMED
|
File without changes
|
|
File without changes
|
{simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/src/simple_openid_connect/data.py
RENAMED
|
File without changes
|
{simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/src/simple_openid_connect/discovery.py
RENAMED
|
File without changes
|
{simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/src/simple_openid_connect/exceptions.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/src/simple_openid_connect/pkce.py
RENAMED
|
File without changes
|
{simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/src/simple_openid_connect/py.typed
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/src/simple_openid_connect/userinfo.py
RENAMED
|
File without changes
|
{simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/src/simple_openid_connect/utils.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/tests/django_test_project/manage.py
RENAMED
|
File without changes
|
{simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/tests/test_authorization_code_flow.py
RENAMED
|
File without changes
|
|
File without changes
|
{simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/tests/test_client_credentials_grant.py
RENAMED
|
File without changes
|
{simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/tests/test_direct_access_grant.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{simple_openid_connect-2.3.2 → simple_openid_connect-2.4.0}/tests/test_rp_initiated_logout.py
RENAMED
|
File without changes
|
|
File without changes
|