simple-module-oidc 0.0.43__py3-none-any.whl

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.
@@ -0,0 +1,231 @@
1
+ Metadata-Version: 2.5
2
+ Name: simple_module_oidc
3
+ Version: 0.0.43
4
+ Summary: Generic OIDC authentication provider for simple_module — native Microsoft Entra ID, Auth0, Okta, Keycloak, and any OpenID Connect identity provider
5
+ Project-URL: Homepage, https://github.com/antosubash/simple_module_python
6
+ Project-URL: Repository, https://github.com/antosubash/simple_module_python
7
+ Author-email: Anto Subash <antosubash@live.com>
8
+ License-Expression: MIT
9
+ Keywords: authentication,azure-ad,entra,fastapi,oidc,simple-module
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Framework :: FastAPI
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Topic :: Internet :: WWW/HTTP
18
+ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
19
+ Classifier: Typing :: Typed
20
+ Requires-Python: >=3.12
21
+ Requires-Dist: cryptography>=42
22
+ Requires-Dist: httpx<0.29,>=0.27
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
28
+ Description-Content-Type: text/markdown
29
+
30
+ # simple_module_oidc
31
+
32
+ Generic **OpenID Connect** authentication provider for simple_module — with
33
+ **native Microsoft Entra ID (Azure AD)** support, plus Auth0, Okta, Zitadel,
34
+ Authentik, Keycloak, and any OIDC-compliant identity provider.
35
+
36
+ The module is configured from a provider's OIDC **discovery document**
37
+ (`.well-known/openid-configuration`), so endpoints (authorize / token / JWKS /
38
+ logout) and the issuer are resolved automatically — no per-provider URL
39
+ templating. Provider differences (claim names, scopes) are config; **presets**
40
+ fill those in for you.
41
+
42
+ It auto-registers as the application's auth provider. Browser users are
43
+ redirected to the IdP's hosted login; mobile/API clients send a bearer token
44
+ which is validated against the provider's JWKS when `api_audience` is configured.
45
+
46
+ ## Install
47
+
48
+ Add to your app's `pyproject.toml` dependencies instead of another auth provider
49
+ (e.g. `simple_module_users` / `simple_module_keycloak` — only one auth provider
50
+ can be active):
51
+
52
+ ```toml
53
+ dependencies = [
54
+ "simple_module_oidc",
55
+ ]
56
+ ```
57
+
58
+ Run `uv sync --all-packages` to install.
59
+
60
+ ## Usage
61
+
62
+ Select `SM_AUTH_PROVIDER=oidc` explicitly. Keep `Auth` enabled; `Settings` UI
63
+ is optional. Exclude Users-dependent modules when selecting this provider.
64
+
65
+ ## Keycloak through the generic preset
66
+
67
+ This is the primary worked example. The dedicated `keycloak` module is an
68
+ alternative; this configuration uses `oidc` throughout.
69
+
70
+ Create an OpenID Connect client in the Keycloak realm:
71
+
72
+ - Enable client authentication (a confidential client) and the standard flow.
73
+ - Set the valid redirect URI to `<app>/api/oidc/auth/callback`.
74
+ - Set the valid post-logout redirect URI to `<app>/oidc/logged-out`.
75
+ - Require PKCE with code challenge method **S256**.
76
+ - Add realm roles to the **ID token**. Keycloak 26 puts `realm_access.roles`
77
+ only in the access token by default, and this module reads roles from the ID
78
+ token. Under Client scopes -> `roles` -> Mappers -> `realm roles`, turn on
79
+ **Add to ID token** (token claim name `realm_access.roles`, multivalued), or
80
+ add a **User Realm Role** mapper with those settings to the client's
81
+ dedicated scope. Then assign the intended realm roles (for example `admin`)
82
+ to users.
83
+ - Keep the default `profile` and `email` client scopes: they supply
84
+ `preferred_username`, `name`, `email` and `email_verified` in the ID token.
85
+
86
+ Use the realm's discovery document and the client's credentials:
87
+
88
+ ```dotenv
89
+ SM_AUTH_PROVIDER=oidc
90
+ SM_OIDC_PROVIDER=generic
91
+ SM_OIDC_DISCOVERY_URL=https://<server>/realms/<realm>/.well-known/openid-configuration
92
+ SM_OIDC_CLIENT_ID=<confidential-client-id>
93
+ SM_OIDC_CLIENT_SECRET=<client-secret>
94
+ SM_OIDC_ROLES_CLAIM_PATH=realm_access.roles
95
+ ```
96
+
97
+ `SM_OIDC_ROLES_CLAIM_PATH` seeds the roles path so the first Keycloak admin is
98
+ mapped to `admin` on first sign-in (the Settings UI that edits it needs an
99
+ admin). A value saved in the UI later wins.
100
+
101
+ Here `<app>` includes the application's scheme and host, and `<server>` is the
102
+ Keycloak server (including any deployment prefix). Discovery is exactly
103
+ `<server>/realms/<realm>/.well-known/openid-configuration` when `<server>` already
104
+ includes the scheme. Use HTTPS outside development.
105
+
106
+ Set these **DB settings** under the `oidc` namespace, using Settings → OIDC when
107
+ the Settings UI is enabled:
108
+
109
+ ```json
110
+ {
111
+ "role_mapping": {"admin": "admin", "user": "user"}
112
+ }
113
+ ```
114
+
115
+ Choose external roles and app roles appropriate for your host. `role_mapping`
116
+ produces application roles; permission grants still come from the framework's
117
+ permission registry. No mapping grants `tenant:*` roles. Visit `/oidc/login` to
118
+ start sign-in. Cached identity is `(issuer, sub)` with a stable framework UUID.
119
+
120
+ ## Native Microsoft Entra ID
121
+
122
+ Set the `entra` preset and your single-tenant registration details:
123
+
124
+ ```bash
125
+ SM_AUTH_PROVIDER=oidc
126
+ SM_OIDC_PROVIDER=entra
127
+ SM_OIDC_TENANT_ID=<your-tenant-guid>
128
+ SM_OIDC_CLIENT_ID=<app-registration-client-id>
129
+ SM_OIDC_CLIENT_SECRET=<client-secret>
130
+ ```
131
+
132
+ In the Entra app registration, add the redirect URI:
133
+
134
+ ```
135
+ https://<your-host>/api/oidc/auth/callback
136
+ ```
137
+
138
+ The `entra` preset derives the discovery URL
139
+ (`https://login.microsoftonline.com/{tenant_id}/v2.0/.well-known/openid-configuration`),
140
+ validates the **id_token**, keys the user cache on **(issuer, `oid`)**, and
141
+ maps Entra **app roles** (the `roles` claim) to application roles via
142
+ `role_mapping` (default: `admin` and `user` map 1:1). Assign app roles in
143
+ *Entra → App registrations → App roles* and *Enterprise applications → Users and groups*.
144
+
145
+ ## Any other OIDC provider
146
+
147
+ Use the `generic` preset with an explicit discovery URL:
148
+
149
+ ```bash
150
+ SM_AUTH_PROVIDER=oidc
151
+ SM_OIDC_PROVIDER=generic
152
+ SM_OIDC_DISCOVERY_URL=https://<issuer>/.well-known/openid-configuration
153
+ SM_OIDC_CLIENT_ID=<client-id>
154
+ SM_OIDC_CLIENT_SECRET=<client-secret>
155
+ ```
156
+
157
+ ## Configuration reference
158
+
159
+ | Env var | Default | Notes |
160
+ |---|---|---|
161
+ | `SM_OIDC_PROVIDER` | `generic` | Preset name (`entra`, `generic`). |
162
+ | `SM_OIDC_DISCOVERY_URL` | — | Explicit discovery URL; wins over the preset. |
163
+ | `SM_OIDC_TENANT_ID` | — | Used by templated presets (Entra) to derive the discovery URL. |
164
+ | `SM_OIDC_CLIENT_ID` | — | OAuth client id. |
165
+ | `SM_OIDC_CLIENT_SECRET` | — | OAuth client secret. |
166
+ | `SM_OIDC_ROLES_CLAIM_PATH` | preset | Seeds `roles_claim_path`, so the first admin can sign in before the Settings UI is reachable. |
167
+
168
+ Only the six environment variables above bootstrap process wiring, secrets and the first admin's roles path.
169
+ These variables seed defaults; explicit DB values can override them.
170
+ All other settings are DB-backed; bare environment names and the old claim
171
+ mapping environment variables are ignored. Provider, discovery, tenant, client
172
+ credentials and JWKS cache TTL changes require a restart. Claim and role mapping
173
+ and tenant-claim trust read the live settings state.
174
+
175
+ | DB setting | Default | Behaviour |
176
+ |---|---|---|
177
+ | `api_audience` | empty | Bearer authentication disabled until configured; ID tokens always use `client_id`. |
178
+ | `api_allowed_client_ids` | empty | Clients besides `client_id` whose API tokens (`azp`, or `appid` on Entra v1) are accepted; a token naming no client is refused. |
179
+ | `trust_tenant_claim` | false | Opt in only to a trusted, validated `tenant_id` claim. Entra `tid` never selects an application tenant. Membership resolution stays authoritative. |
180
+ | `scope` | preset | Entra requests `openid email profile offline_access`. |
181
+ | `uid_claim` | `oid` for Entra, otherwise `sub` | Required identity claim; there is no fallback. Identities use unique (issuer, subject) UUID mappings. |
182
+ | `username_claim` / `name_claim` | `preferred_username` / `name` | Display name with fallback. |
183
+ | `email_claim` | `email` | Display email; only generic OIDC literal `email_verified=true` attests ownership. Entra never attests it. |
184
+ | `roles_claim_path` | `roles` for Entra, otherwise empty | Dotted claim path to role list. |
185
+ | `role_mapping` | admin/user map to themselves | External roles to application roles; `tenant:*` roles are filtered on login and restored sessions. |
186
+ | `login_redirect_url` | `/dashboard/` | Blank values normalize to the default. Saved deep links must be local paths. |
187
+ | `jwks_cache_ttl_seconds` | 3600 | Last-good signing keys retained on outage; unknown keys share a 30-second fetch cooldown. |
188
+
189
+ ## Login, sessions and logout
190
+
191
+ Entra supports a single tenant GUID. `common`, `organizations` and `consumers`
192
+ are rejected. Unknown presets are validation errors. Discovery requires an issuer
193
+ matching the configured discovery URL and HTTPS outside development; different
194
+ endpoint hosts are supported. Discovery failure refuses boot outside development;
195
+ development retries lazily on login.
196
+
197
+ Authorization uses code flow with S256 PKCE, state and constant-time nonce
198
+ validation. JWTs require `iss`, `sub`, `aud`, `exp` and `iat`, RS256 and the
199
+ expected audience. Additional audiences are rejected, and a supplied `azp` on a browser ID token must name this client (required with multiple
200
+ audiences); an API access token's `azp` is the calling client, checked against `api_allowed_client_ids` instead. The returned access token is unused, so `at_hash` is intentionally
201
+ not checked.
202
+
203
+ Sign-in replaces user-bound session state and rotates CSRF. The cookie never
204
+ stores the ID token; it stores the ID-token expiry and an encrypted refresh token
205
+ when supplied. Expired or unbounded sessions renew using a fresh ID token, with
206
+ the same issuer and configured subject required. Definitive refresh rejection
207
+ clears authentication. Providers that do not return an `id_token` on refresh
208
+ sign the user out at expiry. Transport failures, 5xx and retryable 4xx preserve the
209
+ expired session for another attempt, while this request stays unauthenticated;
210
+ the token endpoint is not called again for 30 seconds, so a flapping IdP does not
211
+ stall every request. A refresh token too large for the 4 KB session cookie (Entra
212
+ can issue such tokens) is not stored, and that session ends at ID-token expiry.
213
+
214
+ Logout best-effort revokes the refresh token when discovery advertises a
215
+ `revocation_endpoint`, then always clears local state. The optional discovered logout endpoint gets
216
+ `client_id` and `post_logout_redirect_uri`, without `id_token_hint`. Register
217
+ `https://<your-host>/oidc/logged-out` as the post-logout URI. Providers requiring
218
+ an ID-token hint may show a confirmation prompt or leave their SSO session active;
219
+ the local signed-out page never automatically signs in.
220
+
221
+ The host migration `c9d2e4f6a810` (`oidc@head`, #192) creates the identity cache.
222
+ Apply host migrations with `upgrade heads` before deployment.
223
+
224
+ ## Activation
225
+
226
+ Add `simple_module_oidc` to the host's `pyproject.toml` dependencies and
227
+ `[tool.uv.sources]`, remove the competing provider, then `uv sync --all-packages`.
228
+ Alternatively scope active modules with `SM_MODULES_ENABLED`.
229
+
230
+ See [the full configuration reference](../../docs/modules/oidc.md)
231
+ for every setting, its environment support, tenancy policy and deployment limits.
@@ -0,0 +1,30 @@
1
+ oidc/__init__.py,sha256=w6B07sNeXuaIBPzKZrEhOjyDI1jVkCo5GsUbX1EF-Qs,88
2
+ oidc/client.py,sha256=wHuO4VYaoHLLKpnjImPQyVwub8nkjKZVtKkoy1_NRXg,5214
3
+ oidc/constants.py,sha256=6zrwTZ-_3tkHRltvrHc1fSp5Gtx7NNkQ3seOxvOti7Y,671
4
+ oidc/directory.py,sha256=2J3QNF_tfGkuxHUWbCggKw5_mRwhjIi8lGjCeHwssXw,269
5
+ oidc/discovery.py,sha256=lMtbaRVT3-LBN81KWEcbLSUL9aPMCJLlxNDMhSysn1I,4253
6
+ oidc/jwks.py,sha256=I70JweKu06N79PGm0pb-SwQ18RoOvn1fEU2sahR_F_w,128
7
+ oidc/models.py,sha256=2xMsHJw1J4eAnjVVZZZES2af7vP62UaRQfvd56iaPjs,852
8
+ oidc/module.py,sha256=T72xz0lJNLsHQj8AOjOPYHL2WQUqmuDRL9aSxzKiC1w,2710
9
+ oidc/persistence.py,sha256=SFBOo099EJX4QrQhnARn5Ym430HSk0CwTqFYEUbOH6g,918
10
+ oidc/presets.py,sha256=GIgBF94kNAD16h8l1CGzYnFwriKrXl4K7wGP1-M6J38,2220
11
+ oidc/provider.py,sha256=JKZFS1Bg819QMgYP4N3wOdz6SDlvHXhLOAzykwiJFEU,7357
12
+ oidc/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
+ oidc/renewal.py,sha256=zFjtGEi49IyMrkvO-fvpG61Io6KO6R7zNPj-IoJIvcI,2833
14
+ oidc/runtime.py,sha256=omSdjYDeRMBCG8zLfl-SjJIyp-tFYmrI0uCwx7c3tIc,3136
15
+ oidc/settings.py,sha256=vgvyiw9A6by4mym_cVy4ypYDeTiN5KSM35TWuYXZ_3k,5291
16
+ oidc/state.py,sha256=-nAVX9TVbBBuq5vVwovAIL8ufl5s8_MvTPu28qW9cZU,686
17
+ oidc/tokens.py,sha256=C6zHFtXpoyuCJzY5N0zUrUKN9k2Znslpa_9qHU7QCmQ,434
18
+ oidc/contracts/__init__.py,sha256=1D6ZD67wkpLbG9lbdZUp-t8yinc0mvRMH4ITwh0g74A,29
19
+ oidc/endpoints/__init__.py,sha256=ttqCgpzrvKYU5Mp3EyRmGbhbS17EHlVX4HTZ-8onKHM,29
20
+ oidc/endpoints/api.py,sha256=_Org2zvY73RrkrzEtHJizLldqJ5mo7Z8-Pff7pP5D4k,4814
21
+ oidc/endpoints/views.py,sha256=SvmT68dIiAjyrGZ_DheD_zjNQ9N22f71GdwuE78H82s,2300
22
+ oidc/locales/de.json,sha256=nVJqlgY2-VmhlElK4ZYaRij2aosUZnKXaLcp4aqoCGk,334
23
+ oidc/locales/en.json,sha256=HwSugraKfXwB5ZnHc3g1xSvw79GJkeDQWu3NkDLDc7c,311
24
+ oidc/pages/LoggedOut.tsx,sha256=-f1uptDbo5Q5Vb3oronHcnSAEnaFyGY6Y4nuXNPEX2Y,545
25
+ oidc/pages/Login.tsx,sha256=Hgba4KG3UZP2A3lYb990608FDOAEqFigFZMKoi869mo,579
26
+ oidc/package.json,sha256=G9V3rxo23NdYnKUnDQwzWwZC1LYLx4OeulQlQNhTrdY,120
27
+ simple_module_oidc-0.0.43.dist-info/METADATA,sha256=BdWNKHeavLqHuCV1NYl9jN9wHYgTcGTwkaedBdXEeXI,11314
28
+ simple_module_oidc-0.0.43.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
29
+ simple_module_oidc-0.0.43.dist-info/entry_points.txt,sha256=eC-1TmZoIevuaaMN9SYIuXZiNMOvLPdU4Km-kh88jNA,46
30
+ simple_module_oidc-0.0.43.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [simple_module]
2
+ oidc = oidc.module:OidcModule