simple-module-oidc 0.0.43__tar.gz → 0.0.44__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.
Files changed (66) hide show
  1. {simple_module_oidc-0.0.43 → simple_module_oidc-0.0.44}/PKG-INFO +5 -5
  2. simple_module_oidc-0.0.44/oidc/core/__init__.py +4 -0
  3. {simple_module_oidc-0.0.43/oidc → simple_module_oidc-0.0.44/oidc/core}/runtime.py +4 -4
  4. {simple_module_oidc-0.0.43/oidc → simple_module_oidc-0.0.44/oidc/core}/settings.py +2 -2
  5. {simple_module_oidc-0.0.43/oidc → simple_module_oidc-0.0.44/oidc/core}/state.py +4 -4
  6. simple_module_oidc-0.0.44/oidc/identity/__init__.py +5 -0
  7. {simple_module_oidc-0.0.43/oidc → simple_module_oidc-0.0.44/oidc/identity}/provider.py +27 -6
  8. {simple_module_oidc-0.0.43/oidc → simple_module_oidc-0.0.44/oidc/identity}/renewal.py +3 -3
  9. {simple_module_oidc-0.0.43/oidc → simple_module_oidc-0.0.44/oidc/identity}/tokens.py +1 -1
  10. simple_module_oidc-0.0.44/oidc/jwks.py +11 -0
  11. {simple_module_oidc-0.0.43 → simple_module_oidc-0.0.44}/oidc/module.py +8 -8
  12. simple_module_oidc-0.0.44/oidc/provider.py +11 -0
  13. simple_module_oidc-0.0.44/oidc/settings.py +11 -0
  14. simple_module_oidc-0.0.44/oidc/web/__init__.py +1 -0
  15. {simple_module_oidc-0.0.43/oidc → simple_module_oidc-0.0.44/oidc/web}/endpoints/api.py +7 -8
  16. {simple_module_oidc-0.0.43/oidc → simple_module_oidc-0.0.44/oidc/web}/endpoints/views.py +9 -4
  17. {simple_module_oidc-0.0.43 → simple_module_oidc-0.0.44}/package.json +1 -1
  18. {simple_module_oidc-0.0.43 → simple_module_oidc-0.0.44}/pyproject.toml +5 -5
  19. {simple_module_oidc-0.0.43 → simple_module_oidc-0.0.44}/tests/conftest.py +1 -1
  20. {simple_module_oidc-0.0.43/tests → simple_module_oidc-0.0.44/tests/core/module}/test_oidc_boot.py +4 -4
  21. {simple_module_oidc-0.0.43/tests → simple_module_oidc-0.0.44/tests/core/module}/test_oidc_module.py +1 -1
  22. {simple_module_oidc-0.0.43/tests → simple_module_oidc-0.0.44/tests/core/runtime}/test_oidc_settings_runtime.py +4 -4
  23. {simple_module_oidc-0.0.43/tests → simple_module_oidc-0.0.44/tests/core/settings}/test_oidc_settings_presets.py +2 -2
  24. {simple_module_oidc-0.0.43/tests → simple_module_oidc-0.0.44/tests/identity/client}/test_oidc_client.py +1 -1
  25. {simple_module_oidc-0.0.43/tests → simple_module_oidc-0.0.44/tests/identity/discovery}/test_oidc_discovery.py +1 -1
  26. {simple_module_oidc-0.0.43/tests → simple_module_oidc-0.0.44/tests/identity/jwks}/test_oidc_jwks.py +1 -1
  27. {simple_module_oidc-0.0.43/tests → simple_module_oidc-0.0.44/tests/identity/jwks}/test_oidc_jwks_resilience.py +1 -1
  28. {simple_module_oidc-0.0.43/tests → simple_module_oidc-0.0.44/tests/identity/persistence}/test_oidc_cache_persistence.py +3 -3
  29. {simple_module_oidc-0.0.43/tests → simple_module_oidc-0.0.44/tests/identity/provider}/test_oidc_jwt_policy.py +1 -1
  30. {simple_module_oidc-0.0.43/tests → simple_module_oidc-0.0.44/tests/identity/provider}/test_oidc_legacy_authority.py +3 -3
  31. {simple_module_oidc-0.0.43/tests → simple_module_oidc-0.0.44/tests/identity/provider}/test_oidc_provider.py +2 -2
  32. simple_module_oidc-0.0.44/tests/identity/provider/test_oidc_sign_in_from_claims.py +39 -0
  33. {simple_module_oidc-0.0.43/tests → simple_module_oidc-0.0.44/tests/identity/provider}/test_oidc_tenant_resolver.py +2 -2
  34. {simple_module_oidc-0.0.43/tests → simple_module_oidc-0.0.44/tests/identity/renewal}/test_oidc_refresh.py +5 -5
  35. {simple_module_oidc-0.0.43/tests → simple_module_oidc-0.0.44/tests/identity/renewal}/test_oidc_session_transaction.py +4 -4
  36. {simple_module_oidc-0.0.43/tests → simple_module_oidc-0.0.44/tests/web/endpoints}/test_oidc_authorization.py +3 -3
  37. {simple_module_oidc-0.0.43/tests → simple_module_oidc-0.0.44/tests/web/endpoints}/test_oidc_authorization_query.py +2 -2
  38. {simple_module_oidc-0.0.43/tests → simple_module_oidc-0.0.44/tests/web/endpoints}/test_oidc_endpoint_auth.py +8 -8
  39. {simple_module_oidc-0.0.43/tests → simple_module_oidc-0.0.44/tests/web/endpoints}/test_oidc_logout.py +1 -1
  40. {simple_module_oidc-0.0.43 → simple_module_oidc-0.0.44}/.gitignore +0 -0
  41. {simple_module_oidc-0.0.43 → simple_module_oidc-0.0.44}/README.md +0 -0
  42. {simple_module_oidc-0.0.43 → simple_module_oidc-0.0.44}/oidc/__init__.py +0 -0
  43. {simple_module_oidc-0.0.43 → simple_module_oidc-0.0.44}/oidc/contracts/__init__.py +0 -0
  44. {simple_module_oidc-0.0.43/oidc → simple_module_oidc-0.0.44/oidc/core}/constants.py +0 -0
  45. {simple_module_oidc-0.0.43/oidc → simple_module_oidc-0.0.44/oidc/core}/presets.py +0 -0
  46. {simple_module_oidc-0.0.43/oidc → simple_module_oidc-0.0.44/oidc/identity}/client.py +0 -0
  47. {simple_module_oidc-0.0.43/oidc → simple_module_oidc-0.0.44/oidc/identity}/directory.py +0 -0
  48. {simple_module_oidc-0.0.43/oidc → simple_module_oidc-0.0.44/oidc/identity}/discovery.py +0 -0
  49. {simple_module_oidc-0.0.43/oidc → simple_module_oidc-0.0.44/oidc/identity}/jwks.py +0 -0
  50. {simple_module_oidc-0.0.43/oidc → simple_module_oidc-0.0.44/oidc/identity}/persistence.py +0 -0
  51. {simple_module_oidc-0.0.43 → simple_module_oidc-0.0.44}/oidc/locales/de.json +0 -0
  52. {simple_module_oidc-0.0.43 → simple_module_oidc-0.0.44}/oidc/locales/en.json +0 -0
  53. {simple_module_oidc-0.0.43 → simple_module_oidc-0.0.44}/oidc/models.py +0 -0
  54. {simple_module_oidc-0.0.43 → simple_module_oidc-0.0.44}/oidc/pages/LoggedOut.tsx +0 -0
  55. {simple_module_oidc-0.0.43 → simple_module_oidc-0.0.44}/oidc/pages/Login.tsx +0 -0
  56. {simple_module_oidc-0.0.43 → simple_module_oidc-0.0.44}/oidc/py.typed +0 -0
  57. {simple_module_oidc-0.0.43/oidc → simple_module_oidc-0.0.44/oidc/web}/endpoints/__init__.py +0 -0
  58. {simple_module_oidc-0.0.43/tests → simple_module_oidc-0.0.44/tests/core/module}/test_oidc_migration.py +0 -0
  59. {simple_module_oidc-0.0.43/tests → simple_module_oidc-0.0.44/tests/identity/directory}/test_oidc_directory.py +0 -0
  60. {simple_module_oidc-0.0.43/tests → simple_module_oidc-0.0.44/tests/identity/provider}/test_oidc_bearer_policy.py +0 -0
  61. {simple_module_oidc-0.0.43 → simple_module_oidc-0.0.44}/tests/oidc_flow_helpers.py +0 -0
  62. {simple_module_oidc-0.0.43/tests → simple_module_oidc-0.0.44/tests/web/endpoints}/test_oidc_flow.py +0 -0
  63. {simple_module_oidc-0.0.43/tests → simple_module_oidc-0.0.44/tests/web/endpoints}/test_oidc_keycloak_flow.py +0 -0
  64. {simple_module_oidc-0.0.43 → simple_module_oidc-0.0.44}/tests-js/oidc-login.test.tsx +0 -0
  65. {simple_module_oidc-0.0.43 → simple_module_oidc-0.0.44}/tests-js/oidc-translations.test.tsx +0 -0
  66. {simple_module_oidc-0.0.43 → simple_module_oidc-0.0.44}/tsconfig.json +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: simple_module_oidc
3
- Version: 0.0.43
3
+ Version: 0.0.44
4
4
  Summary: Generic OIDC authentication provider for simple_module — native Microsoft Entra ID, Auth0, Okta, Keycloak, and any OpenID Connect identity provider
5
5
  Project-URL: Homepage, https://github.com/antosubash/simple_module_python
6
6
  Project-URL: Repository, https://github.com/antosubash/simple_module_python
@@ -21,10 +21,10 @@ Requires-Python: >=3.12
21
21
  Requires-Dist: cryptography>=42
22
22
  Requires-Dist: httpx<0.29,>=0.27
23
23
  Requires-Dist: pyjwt[crypto]<3,>=2.8
24
- Requires-Dist: simple-module-auth==0.0.43
25
- Requires-Dist: simple-module-core==0.0.43
26
- Requires-Dist: simple-module-db==0.0.43
27
- Requires-Dist: simple-module-hosting==0.0.43
24
+ Requires-Dist: simple-module-auth==0.0.44
25
+ Requires-Dist: simple-module-core==0.0.44
26
+ Requires-Dist: simple-module-db==0.0.44
27
+ Requires-Dist: simple-module-hosting==0.0.44
28
28
  Description-Content-Type: text/markdown
29
29
 
30
30
  # simple_module_oidc
@@ -0,0 +1,4 @@
1
+ """What the OIDC provider builds on.
2
+
3
+ Constants, settings, presets, app state and the app-free runtime.
4
+ """
@@ -4,10 +4,10 @@ import logging
4
4
 
5
5
  from fastapi import HTTPException, Request
6
6
 
7
- from oidc.client import OIDCClient
8
- from oidc.constants import SESSION_USER_KEY
9
- from oidc.discovery import fetch_metadata
10
- from oidc.jwks import JWKSCache
7
+ from oidc.core.constants import SESSION_USER_KEY
8
+ from oidc.identity.client import OIDCClient
9
+ from oidc.identity.discovery import fetch_metadata
10
+ from oidc.identity.jwks import JWKSCache
11
11
 
12
12
  logger = logging.getLogger(__name__)
13
13
  _DISCOVERY_SUFFIX = "/.well-known/openid-configuration"
@@ -17,8 +17,8 @@ from simple_module_core.redirect_safety import non_empty_redirect
17
17
  from simple_module_core.settings_base import DbBackedSettings
18
18
  from simple_module_core.stored_settings import field_error
19
19
 
20
- from oidc.constants import DEFAULT_LOGIN_REDIRECT_URL
21
- from oidc.presets import get_preset
20
+ from oidc.core.constants import DEFAULT_LOGIN_REDIRECT_URL
21
+ from oidc.core.presets import get_preset
22
22
 
23
23
  # Sentinel for "unset by the user" so the preset can supply a default while still
24
24
  # letting an explicit empty/other value win.
@@ -7,10 +7,10 @@ from dataclasses import dataclass, field
7
7
  from typing import TYPE_CHECKING
8
8
 
9
9
  if TYPE_CHECKING:
10
- from oidc.client import OIDCClient
11
- from oidc.discovery import OidcMetadata
12
- from oidc.jwks import JWKSCache
13
- from oidc.settings import OidcSettings
10
+ from oidc.core.settings import OidcSettings
11
+ from oidc.identity.client import OIDCClient
12
+ from oidc.identity.discovery import OidcMetadata
13
+ from oidc.identity.jwks import JWKSCache
14
14
 
15
15
 
16
16
  @dataclass
@@ -0,0 +1,5 @@
1
+ """The auth provider.
2
+
3
+ Discovery, OIDC client, token renewal and encryption, JWKS, the user directory
4
+ and its cache persistence.
5
+ """
@@ -12,18 +12,22 @@ from auth.oidc_common.tenant import trusted_tenant, without_tenant_roles
12
12
  from simple_module_hosting.session import SESSION_EXPIRES_AT_KEY, session_has_expired
13
13
  from starlette.requests import Request
14
14
 
15
- from oidc.constants import LOGIN_PATH, LOGOUT_PATH, SESSION_USER_KEY
16
- from oidc.persistence import CachePersistence
17
- from oidc.renewal import SessionRenewal
15
+ from oidc.core.constants import LOGIN_PATH, LOGOUT_PATH, SESSION_USER_KEY
16
+ from oidc.identity.persistence import CachePersistence
17
+ from oidc.identity.renewal import SessionRenewal
18
18
 
19
19
  if TYPE_CHECKING:
20
- from oidc.jwks import JWKSCache
21
- from oidc.settings import OidcSettings
22
- from oidc.state import OidcState
20
+ from oidc.core.settings import OidcSettings
21
+ from oidc.core.state import OidcState
22
+ from oidc.identity.jwks import JWKSCache
23
23
 
24
24
  logger = logging.getLogger(__name__)
25
25
 
26
26
 
27
+ class IdentityUnavailableError(Exception):
28
+ """The user cache could not record the signed-in identity."""
29
+
30
+
27
31
  class OidcAuthProvider(CachePersistence, SessionRenewal):
28
32
  """Generic OIDC authentication with issuer-scoped identities."""
29
33
 
@@ -113,6 +117,23 @@ class OidcAuthProvider(CachePersistence, SessionRenewal):
113
117
  logger.info("OIDC bearer token from a client not in api_allowed_client_ids")
114
118
  return False
115
119
 
120
+ async def sign_in_from_claims(
121
+ self, request: Request, claims: dict[str, Any]
122
+ ) -> tuple[UserContext, tuple[str, str]]:
123
+ """Turn verified ID-token claims into a browser sign-in.
124
+
125
+ Returns the session ``UserContext`` and the ``(issuer, subject)``
126
+ identity to pin the session to. The caller has already validated the
127
+ token, nonce and expiry. Raises ``ValueError`` when the claims carry no
128
+ usable identity and ``IdentityUnavailableError`` when the user cache
129
+ refuses it.
130
+ """
131
+ identity = self._identity(claims)
132
+ cache_id = await self._upsert_user_cache(request, claims)
133
+ if cache_id is None:
134
+ raise IdentityUnavailableError("OIDC identity unavailable")
135
+ return self._claims_to_user_context(claims, cache_id=cache_id), identity
136
+
116
137
  def _claims_to_user_context(
117
138
  self,
118
139
  claims: dict[str, Any],
@@ -3,14 +3,14 @@
3
3
  from auth.oidc_common.renewal import refresh_session
4
4
  from auth.oidc_common.session import SessionKeys, accept_authority, authority_marker
5
5
 
6
- from oidc.constants import (
6
+ from oidc.core.constants import (
7
7
  SESSION_IDENTITY_KEY,
8
8
  SESSION_REFRESH_RETRY_AT_KEY,
9
9
  SESSION_REFRESH_TOKEN_KEY,
10
10
  SESSION_USER_KEY,
11
11
  )
12
- from oidc.runtime import require_client
13
- from oidc.tokens import open_refresh_token, store_refresh_token
12
+ from oidc.core.runtime import require_client
13
+ from oidc.identity.tokens import open_refresh_token, store_refresh_token
14
14
 
15
15
  _KEYS = SessionKeys(
16
16
  SESSION_USER_KEY,
@@ -2,7 +2,7 @@
2
2
 
3
3
  from auth.oidc_common.tokens import provider_tokens
4
4
 
5
- from oidc.constants import SESSION_REFRESH_TOKEN_KEY
5
+ from oidc.core.constants import SESSION_REFRESH_TOKEN_KEY
6
6
 
7
7
  _TOKENS = provider_tokens(info=b"oidc-refresh-token", session_key=SESSION_REFRESH_TOKEN_KEY)
8
8
  seal_refresh_token = _TOKENS.seal_refresh_token
@@ -0,0 +1,11 @@
1
+ """Moved to :mod:`oidc.identity.jwks`; this alias goes in 0.0.46 (module layout)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib
6
+ import sys
7
+
8
+ from simple_module_core.deprecation import deprecated_module
9
+
10
+ deprecated_module("oidc.jwks", remove_in="0.0.46", use="oidc.identity.jwks")
11
+ sys.modules[__name__] = importlib.import_module("oidc.identity.jwks")
@@ -11,7 +11,7 @@ from simple_module_core.module import ModuleBase, ModuleMeta
11
11
  from simple_module_core.module_settings import register_module_settings
12
12
  from simple_module_core.user_directory import set_user_directory
13
13
 
14
- from oidc.constants import (
14
+ from oidc.core.constants import (
15
15
  _MODULE_AUTH,
16
16
  _MODULE_SETTINGS,
17
17
  AUTH_CALLBACK_PATH,
@@ -36,10 +36,10 @@ class OidcModule(ModuleBase):
36
36
  _is_auth_provider = True
37
37
 
38
38
  def register_settings(self, app: FastAPI) -> None:
39
- from oidc.directory import OidcDirectory
40
- from oidc.provider import OidcAuthProvider
41
- from oidc.settings import OidcSettings
42
- from oidc.state import OidcState
39
+ from oidc.core.settings import OidcSettings
40
+ from oidc.core.state import OidcState
41
+ from oidc.identity.directory import OidcDirectory
42
+ from oidc.identity.provider import OidcAuthProvider
43
43
 
44
44
  register_module_settings(
45
45
  app,
@@ -67,8 +67,8 @@ class OidcModule(ModuleBase):
67
67
  )
68
68
 
69
69
  def register_routes(self, api_router: APIRouter, view_router: APIRouter) -> None:
70
- from oidc.endpoints.api import router as api
71
- from oidc.endpoints.views import router as views
70
+ from oidc.web.endpoints.api import router as api
71
+ from oidc.web.endpoints.views import router as views
72
72
 
73
73
  api_router.include_router(api)
74
74
  view_router.include_router(views)
@@ -79,7 +79,7 @@ class OidcModule(ModuleBase):
79
79
  registry.add_exact(LOGOUT_PATH, methods={"POST"})
80
80
 
81
81
  async def on_startup(self, app: FastAPI) -> None:
82
- from oidc.runtime import initialize
82
+ from oidc.core.runtime import initialize
83
83
 
84
84
  await initialize(app)
85
85
 
@@ -0,0 +1,11 @@
1
+ """Moved to :mod:`oidc.identity.provider`; this alias goes in 0.0.46 (module layout)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib
6
+ import sys
7
+
8
+ from simple_module_core.deprecation import deprecated_module
9
+
10
+ deprecated_module("oidc.provider", remove_in="0.0.46", use="oidc.identity.provider")
11
+ sys.modules[__name__] = importlib.import_module("oidc.identity.provider")
@@ -0,0 +1,11 @@
1
+ """Moved to :mod:`oidc.core.settings`; this alias goes in 0.0.46 (module layout)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib
6
+ import sys
7
+
8
+ from simple_module_core.deprecation import deprecated_module
9
+
10
+ deprecated_module("oidc.settings", remove_in="0.0.46", use="oidc.core.settings")
11
+ sys.modules[__name__] = importlib.import_module("oidc.core.settings")
@@ -0,0 +1 @@
1
+ """The HTTP side: the REST and Inertia endpoints."""
@@ -10,15 +10,16 @@ from auth.oidc_common.session import sign_in, stale_callback
10
10
  from fastapi import APIRouter, HTTPException, Request
11
11
  from starlette.responses import RedirectResponse
12
12
 
13
- from oidc.constants import (
13
+ from oidc.core.constants import (
14
14
  SESSION_IDENTITY_KEY,
15
15
  SESSION_NONCE_KEY,
16
16
  SESSION_STATE_KEY,
17
17
  SESSION_USER_KEY,
18
18
  SESSION_VERIFIER_KEY,
19
19
  )
20
- from oidc.runtime import require_client
21
- from oidc.tokens import store_refresh_token
20
+ from oidc.core.runtime import require_client
21
+ from oidc.identity.provider import IdentityUnavailableError
22
+ from oidc.identity.tokens import store_refresh_token
22
23
 
23
24
  logger = logging.getLogger(__name__)
24
25
  router = APIRouter(prefix="/auth", tags=["oidc-auth"])
@@ -97,13 +98,11 @@ async def oidc_callback(request: Request):
97
98
  raise HTTPException(status_code=401, detail="Invalid ID token expiry")
98
99
  provider = request.app.state.auth.auth_provider
99
100
  try:
100
- identity = provider._identity(claims)
101
+ user_ctx, identity = await provider.sign_in_from_claims(request, claims)
101
102
  except ValueError:
102
103
  raise HTTPException(status_code=401, detail="Missing OIDC identity") from None
103
- cache_id = await provider._upsert_user_cache(request, claims)
104
- if cache_id is None:
105
- raise HTTPException(status_code=503, detail="OIDC identity unavailable")
106
- user_ctx = provider._claims_to_user_context(claims, cache_id=cache_id)
104
+ except IdentityUnavailableError:
105
+ raise HTTPException(status_code=503, detail="OIDC identity unavailable") from None
107
106
  next_url = sign_in(
108
107
  request,
109
108
  user=user_ctx,
@@ -8,10 +8,15 @@ from simple_module_hosting.inertia_deps import InertiaDep, is_inertia_request
8
8
  from simple_module_inertia.response import location
9
9
  from starlette.responses import RedirectResponse
10
10
 
11
- from oidc.constants import _PAGE_LOGGED_OUT, _PAGE_LOGIN, LOGGED_OUT_PATH, SESSION_REFRESH_TOKEN_KEY
12
- from oidc.renewal import _KEYS
13
- from oidc.runtime import is_signed_in
14
- from oidc.tokens import open_refresh_token
11
+ from oidc.core.constants import (
12
+ _PAGE_LOGGED_OUT,
13
+ _PAGE_LOGIN,
14
+ LOGGED_OUT_PATH,
15
+ SESSION_REFRESH_TOKEN_KEY,
16
+ )
17
+ from oidc.core.runtime import is_signed_in
18
+ from oidc.identity.renewal import _KEYS
19
+ from oidc.identity.tokens import open_refresh_token
15
20
 
16
21
  logger = logging.getLogger(__name__)
17
22
  router = APIRouter(tags=["oidc-views"])
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@simple-module/oidc",
3
3
  "private": true,
4
- "version": "0.0.43",
4
+ "version": "0.0.44",
5
5
  "type": "module",
6
6
  "dependencies": {}
7
7
  }
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "simple_module_oidc"
3
- version = "0.0.43"
3
+ version = "0.0.44"
4
4
  description = "Generic OIDC authentication provider for simple_module — native Microsoft Entra ID, Auth0, Okta, Keycloak, and any OpenID Connect identity provider"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -20,10 +20,10 @@ classifiers = [
20
20
  "Typing :: Typed",
21
21
  ]
22
22
  dependencies = [
23
- "simple_module_core==0.0.43",
24
- "simple_module_db==0.0.43",
25
- "simple_module_hosting==0.0.43",
26
- "simple_module_auth==0.0.43",
23
+ "simple_module_core==0.0.44",
24
+ "simple_module_db==0.0.44",
25
+ "simple_module_hosting==0.0.44",
26
+ "simple_module_auth==0.0.44",
27
27
  "PyJWT[crypto]>=2.8,<3",
28
28
  "cryptography>=42",
29
29
  "httpx>=0.27,<0.29",
@@ -6,7 +6,7 @@ import httpx
6
6
  import pytest
7
7
  from cryptography.hazmat.primitives.asymmetric import rsa
8
8
  from jwt.algorithms import RSAAlgorithm
9
- from oidc.settings import OidcSettings
9
+ from oidc.core.settings import OidcSettings
10
10
  from oidc_flow_helpers import _AUDIENCE, _KID, _PRIVATE_KEY
11
11
  from simple_module_core.discovery import discover_modules
12
12
  from simple_module_db.base import all_module_bases
@@ -5,11 +5,11 @@ from types import SimpleNamespace
5
5
 
6
6
  import httpx
7
7
  import pytest
8
+ from oidc.core.runtime import require_client
9
+ from oidc.core.settings import OidcSettings
10
+ from oidc.core.state import OidcState
11
+ from oidc.identity.provider import OidcAuthProvider
8
12
  from oidc.module import OidcModule
9
- from oidc.provider import OidcAuthProvider
10
- from oidc.runtime import require_client
11
- from oidc.settings import OidcSettings
12
- from oidc.state import OidcState
13
13
  from starlette.requests import Request
14
14
 
15
15
 
@@ -16,7 +16,7 @@ def test_oidc_module_meta():
16
16
 
17
17
 
18
18
  def test_oidc_provider_satisfies_protocol():
19
- from oidc.provider import OidcAuthProvider
19
+ from oidc.identity.provider import OidcAuthProvider
20
20
 
21
21
  provider = OidcAuthProvider()
22
22
  assert isinstance(provider, AuthProvider)
@@ -1,9 +1,9 @@
1
1
  """OIDC policy uses hydrated settings even without the Settings UI (#192)."""
2
2
 
3
- from oidc.settings import OidcSettings
4
- from settings.invalidation import reload_package
5
- from settings.service import SettingService
6
- from settings.store import SettingsStore
3
+ from oidc.core.settings import OidcSettings
4
+ from settings.core.invalidation import reload_package
5
+ from settings.core.service import SettingService
6
+ from settings.module_settings.store import SettingsStore
7
7
  from simple_module_core.module_settings import module_settings_registry
8
8
 
9
9
 
@@ -3,7 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import pytest
6
- from oidc.settings import OidcSettings
6
+ from oidc.core.settings import OidcSettings
7
7
  from pydantic import ValidationError
8
8
 
9
9
 
@@ -111,7 +111,7 @@ def test_roles_claim_path_is_seeded_from_env_for_the_first_admin():
111
111
  import subprocess
112
112
  import sys
113
113
 
114
- code = "from oidc.settings import OidcSettings; print(OidcSettings().roles_claim_path)"
114
+ code = "from oidc.core.settings import OidcSettings; print(OidcSettings().roles_claim_path)"
115
115
  env = {**os.environ, "SM_OIDC_ROLES_CLAIM_PATH": "realm_access.roles"}
116
116
  out = subprocess.run(
117
117
  [sys.executable, "-c", code], env=env, capture_output=True, text=True, check=True
@@ -3,7 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import pytest
6
- from oidc.client import OIDCClient
6
+ from oidc.identity.client import OIDCClient
7
7
 
8
8
 
9
9
  @pytest.fixture
@@ -3,7 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import pytest
6
- from oidc.discovery import OidcMetadata, fetch_metadata
6
+ from oidc.identity.discovery import OidcMetadata, fetch_metadata
7
7
 
8
8
  _ENTRA_DOC = {
9
9
  "issuer": "https://login.microsoftonline.com/tid/v2.0",
@@ -5,7 +5,7 @@ from __future__ import annotations
5
5
  import time
6
6
 
7
7
  import pytest
8
- from oidc.jwks import JWKSCache
8
+ from oidc.identity.jwks import JWKSCache
9
9
  from oidc_flow_helpers import make_jwks_response, sign_token
10
10
 
11
11
 
@@ -6,7 +6,7 @@ from types import SimpleNamespace
6
6
 
7
7
  import httpx
8
8
  import pytest
9
- from oidc.jwks import JWKSCache
9
+ from oidc.identity.jwks import JWKSCache
10
10
  from oidc_flow_helpers import make_jwks_response, sign_token
11
11
 
12
12
 
@@ -4,9 +4,9 @@ from datetime import UTC, datetime, timedelta
4
4
  from types import SimpleNamespace
5
5
 
6
6
  import pytest
7
+ from oidc.core.settings import OidcSettings
8
+ from oidc.identity.provider import OidcAuthProvider
7
9
  from oidc.models import OidcUserCache
8
- from oidc.provider import OidcAuthProvider
9
- from oidc.settings import OidcSettings
10
10
  from simple_module_db import independent_session
11
11
  from sqlalchemy import event, select
12
12
  from starlette.requests import Request
@@ -145,7 +145,7 @@ async def test_failed_cache_never_falls_back_to_external_subject(cache_request,
145
145
  def broken(*args):
146
146
  raise RuntimeError("database unavailable")
147
147
 
148
- monkeypatch.setattr("oidc.persistence.independent_session", broken)
148
+ monkeypatch.setattr("oidc.identity.persistence.independent_session", broken)
149
149
  assert await OidcAuthProvider()._upsert_user_cache(cache_request, CLAIMS) is None
150
150
 
151
151
 
@@ -5,7 +5,7 @@ import time
5
5
  import jwt
6
6
  import pytest
7
7
  from cryptography.hazmat.primitives.asymmetric import rsa
8
- from oidc.jwks import JWKSCache
8
+ from oidc.identity.jwks import JWKSCache
9
9
 
10
10
 
11
11
  @pytest.fixture
@@ -4,9 +4,9 @@ import time
4
4
 
5
5
  import pytest
6
6
  from auth.contracts.schemas import UserContext
7
- from oidc.jwks import JWKSCache
8
- from oidc.provider import OidcAuthProvider
9
- from oidc.settings import OidcSettings
7
+ from oidc.core.settings import OidcSettings
8
+ from oidc.identity.jwks import JWKSCache
9
+ from oidc.identity.provider import OidcAuthProvider
10
10
  from starlette.requests import Request
11
11
 
12
12
 
@@ -7,8 +7,8 @@ from unittest.mock import MagicMock
7
7
  import pytest
8
8
  from auth.contracts.provider import AuthProvider
9
9
  from auth.contracts.schemas import UserContext
10
- from oidc.provider import OidcAuthProvider
11
- from oidc.settings import OidcSettings
10
+ from oidc.core.settings import OidcSettings
11
+ from oidc.identity.provider import OidcAuthProvider
12
12
 
13
13
 
14
14
  @pytest.fixture
@@ -0,0 +1,39 @@
1
+ """``sign_in_from_claims`` is the public seam the login callback uses (#848)."""
2
+
3
+ from unittest.mock import AsyncMock
4
+
5
+ import pytest
6
+ from oidc.core.settings import OidcSettings
7
+ from oidc.identity.provider import IdentityUnavailableError, OidcAuthProvider
8
+ from starlette.requests import Request
9
+
10
+ CLAIMS = {"iss": "https://idp.test", "sub": "subject", "email": "a@b.test"}
11
+
12
+
13
+ @pytest.fixture
14
+ def provider(monkeypatch):
15
+ provider = OidcAuthProvider(OidcSettings(client_id="app"))
16
+ monkeypatch.setattr(provider, "_upsert_user_cache", AsyncMock(return_value="cache-1"))
17
+ return provider
18
+
19
+
20
+ async def test_returns_context_and_identity(provider):
21
+ request = Request({"type": "http"})
22
+ ctx, identity = await provider.sign_in_from_claims(request, CLAIMS)
23
+ assert identity == ("https://idp.test", "subject")
24
+ assert (
25
+ ctx.to_session_dict()
26
+ == provider._claims_to_user_context(CLAIMS, cache_id="cache-1").to_session_dict()
27
+ )
28
+ provider._upsert_user_cache.assert_awaited_once_with(request, CLAIMS)
29
+
30
+
31
+ async def test_missing_identity_raises_value_error(provider):
32
+ with pytest.raises(ValueError):
33
+ await provider.sign_in_from_claims(Request({"type": "http"}), {"email": "a@b.test"})
34
+
35
+
36
+ async def test_cache_refusal_raises(provider):
37
+ provider._upsert_user_cache.return_value = None
38
+ with pytest.raises(IdentityUnavailableError):
39
+ await provider.sign_in_from_claims(Request({"type": "http"}), CLAIMS)
@@ -6,8 +6,8 @@ Lives apart from ``test_oidc_provider.py`` because that file overrides the
6
6
 
7
7
  from __future__ import annotations
8
8
 
9
- from oidc.provider import OidcAuthProvider
10
- from oidc.settings import OidcSettings
9
+ from oidc.core.settings import OidcSettings
10
+ from oidc.identity.provider import OidcAuthProvider
11
11
  from starlette.requests import Request
12
12
 
13
13
  _USER_ID = "11111111-1111-1111-1111-111111111111"
@@ -7,10 +7,10 @@ from urllib.parse import parse_qs
7
7
  import httpx
8
8
  import pytest
9
9
  from auth.contracts.schemas import UserContext
10
- from oidc.client import OIDCClient
11
- from oidc.provider import OidcAuthProvider
12
- from oidc.settings import OidcSettings
13
- from oidc.tokens import open_refresh_token, seal_refresh_token
10
+ from oidc.core.settings import OidcSettings
11
+ from oidc.identity.client import OIDCClient
12
+ from oidc.identity.provider import OidcAuthProvider
13
+ from oidc.identity.tokens import open_refresh_token, seal_refresh_token
14
14
  from starlette.requests import Request
15
15
 
16
16
 
@@ -203,7 +203,7 @@ async def test_session_deadline_never_slides(refresh_session, monkeypatch):
203
203
 
204
204
 
205
205
  async def test_refresh_sealing_is_provider_scoped(refresh_session):
206
- from keycloak.tokens import open_refresh_token as open_keycloak_token
206
+ from keycloak.identity.tokens import open_refresh_token as open_keycloak_token
207
207
 
208
208
  _, request = refresh_session
209
209
  sealed = request.session["oidc_refresh_token"]
@@ -8,10 +8,10 @@ from unittest.mock import AsyncMock
8
8
  import httpx
9
9
  import pytest
10
10
  from fastapi import HTTPException
11
- from oidc.endpoints import api
12
- from oidc.provider import OidcAuthProvider
13
- from oidc.settings import OidcSettings
14
- from oidc.tokens import open_refresh_token
11
+ from oidc.core.settings import OidcSettings
12
+ from oidc.identity.provider import OidcAuthProvider
13
+ from oidc.identity.tokens import open_refresh_token
14
+ from oidc.web.endpoints import api
15
15
  from simple_module_hosting.csrf import get_csrf_token
16
16
  from simple_module_hosting.session import SessionMiddleware, preserve_across_sign_in
17
17
  from starlette.requests import Request
@@ -5,9 +5,9 @@ from unittest.mock import AsyncMock
5
5
 
6
6
  import pytest
7
7
  from auth.contracts.schemas import UserContext
8
- from oidc.provider import OidcAuthProvider
9
- from oidc.settings import OidcSettings
10
- from oidc.state import OidcState
8
+ from oidc.core.settings import OidcSettings
9
+ from oidc.core.state import OidcState
10
+ from oidc.identity.provider import OidcAuthProvider
11
11
  from starlette.requests import Request
12
12
 
13
13
 
@@ -3,8 +3,8 @@
3
3
  from urllib.parse import parse_qs, urlsplit
4
4
 
5
5
  import pytest
6
- from oidc.client import OIDCClient
7
- from oidc.discovery import OidcMetadata
6
+ from oidc.identity.client import OIDCClient
7
+ from oidc.identity.discovery import OidcMetadata
8
8
 
9
9
 
10
10
  @pytest.mark.parametrize("query", ["", "?tenant=alpha", "?tenant=alpha&policy=a%2Bb"])
@@ -6,11 +6,11 @@ from urllib.parse import parse_qs
6
6
 
7
7
  import httpx
8
8
  import pytest
9
- from oidc.constants import SESSION_REFRESH_TOKEN_KEY
10
- from oidc.runtime import initialize
11
- from oidc.settings import OidcSettings
12
- from oidc.state import OidcState
13
- from oidc.tokens import seal_refresh_token
9
+ from oidc.core.constants import SESSION_REFRESH_TOKEN_KEY
10
+ from oidc.core.runtime import initialize
11
+ from oidc.core.settings import OidcSettings
12
+ from oidc.core.state import OidcState
13
+ from oidc.identity.tokens import seal_refresh_token
14
14
  from starlette.requests import Request
15
15
 
16
16
  _DOC = {
@@ -131,7 +131,7 @@ def _logout_request(app, session):
131
131
  async def test_logout_revokes_before_clearing_even_on_failure(
132
132
  endpoint_app, basic, failure, httpx_mock, caplog
133
133
  ):
134
- from oidc.endpoints.views import logout
134
+ from oidc.web.endpoints.views import logout
135
135
 
136
136
  session = {SESSION_REFRESH_TOKEN_KEY: seal_refresh_token(endpoint_app, "private-refresh")}
137
137
  calls = []
@@ -167,7 +167,7 @@ async def test_logout_revokes_before_clearing_even_on_failure(
167
167
  indirect=["endpoint_app"],
168
168
  )
169
169
  async def test_logout_skips_unavailable_revocation(endpoint_app, token, httpx_mock):
170
- from oidc.endpoints.views import logout
170
+ from oidc.web.endpoints.views import logout
171
171
 
172
172
  session = {}
173
173
  if token:
@@ -203,7 +203,7 @@ async def test_revocation_uses_its_own_advertised_auth_method(endpoint_app, http
203
203
  async def test_revocation_total_time_is_capped(endpoint_app, monkeypatch):
204
204
  import asyncio
205
205
 
206
- from oidc import client as client_module
206
+ from oidc.identity import client as client_module
207
207
 
208
208
  async def hang(*args, **kwargs):
209
209
  await asyncio.Event().wait() # an IdP that never answers
@@ -73,7 +73,7 @@ async def test_logout_drops_the_sessions_shared_renewal_outcome(oidc_app, flow_c
73
73
  import time
74
74
 
75
75
  from auth.models import RefreshOutcome
76
- from oidc.renewal import _KEYS
76
+ from oidc.identity.renewal import _KEYS
77
77
  from simple_module_db import independent_session
78
78
  from sqlmodel import select
79
79