fastapi-keycloak-admin 0.1.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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Alexander Korolev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,607 @@
1
+ Metadata-Version: 2.4
2
+ Name: fastapi-keycloak-admin
3
+ Version: 0.1.0
4
+ Summary: Keycloak authentication, account and user administration for FastAPI applications
5
+ Keywords: authentication,fastapi,keycloak,oauth2,oidc
6
+ Author: Alexander Korolev
7
+ Author-email: Alexander Korolev <alexkorolex@bk.ru>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Framework :: AsyncIO
12
+ Classifier: Framework :: FastAPI
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
20
+ Classifier: Topic :: Internet :: WWW/HTTP
21
+ Classifier: Topic :: Security
22
+ Classifier: Typing :: Typed
23
+ Requires-Dist: aiohttp>=3.9
24
+ Requires-Dist: click>=8.1
25
+ Requires-Dist: fastapi>=0.115
26
+ Requires-Dist: pyjwt[crypto]>=2.8
27
+ Requires-Python: >=3.12
28
+ Project-URL: Changelog, https://github.com/alexkorolex/fastapi_keycloak_admin/blob/main/CHANGELOG.md
29
+ Project-URL: Documentation, https://github.com/alexkorolex/fastapi_keycloak_admin#readme
30
+ Project-URL: Issues, https://github.com/alexkorolex/fastapi_keycloak_admin/issues
31
+ Project-URL: Repository, https://github.com/alexkorolex/fastapi_keycloak_admin
32
+ Description-Content-Type: text/markdown
33
+
34
+ # fastapi-keycloak-admin
35
+
36
+ [![PyPI](https://img.shields.io/pypi/v/fastapi-keycloak-admin)](https://pypi.org/project/fastapi-keycloak-admin/)
37
+ [![Python](https://img.shields.io/pypi/pyversions/fastapi-keycloak-admin)](https://pypi.org/project/fastapi-keycloak-admin/)
38
+ [![CI](https://github.com/alexkorolex/fastapi_keycloak_admin/actions/workflows/ci.yml/badge.svg)](https://github.com/alexkorolex/fastapi_keycloak_admin/actions/workflows/ci.yml)
39
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/alexkorolex/fastapi_keycloak_admin/blob/main/LICENSE)
40
+
41
+ Authentication, account and user management for FastAPI applications that use Keycloak as
42
+ their identity provider. It validates Keycloak access tokens, lets your frontend keep its own
43
+ login form, lets users manage their own account, and lets administrators create users with any
44
+ realm roles. Neither the browser nor your code ever has to talk to Keycloak directly.
45
+
46
+ > The project is currently alpha software. Pin the version, test upgrades in a staging
47
+ > environment, and review the [security notes](https://github.com/alexkorolex/fastapi_keycloak_admin#security-notes) before using it in production.
48
+
49
+ It is the FastAPI counterpart of
50
+ [litestar-keycloak-admin](https://github.com/alexkorolex/litestar_keycloak_admin): the same
51
+ endpoints, the same error format, the same login verification by code.
52
+
53
+ | Concern | Provided as |
54
+ | --- | --- |
55
+ | Validating access tokens (JWKS, `exp`, `iss`, `aud`, `typ`) | `CurrentUser` dependency |
56
+ | Role and scope checks | `require_roles`, `require_client_roles`, `require_scopes`, `requires_admin` |
57
+ | Login with username and password, refresh, logout | built-in endpoints |
58
+ | Replacing a temporary password at first login | built-in endpoints |
59
+ | Own profile and password, optional login code by e-mail | built-in endpoints |
60
+ | Listing realm roles, registering users | built-in endpoints (admin only) |
61
+ | Creating users from the command line (e.g. the first admin) | `fastapi-keycloak-admin create-user` |
62
+
63
+ **Contents:**
64
+ [Installation](https://github.com/alexkorolex/fastapi_keycloak_admin#installation) ·
65
+ [Quick start](https://github.com/alexkorolex/fastapi_keycloak_admin#quick-start) ·
66
+ [Keycloak setup](https://github.com/alexkorolex/fastapi_keycloak_admin#keycloak-setup) ·
67
+ [Protecting routes](https://github.com/alexkorolex/fastapi_keycloak_admin#protecting-routes) ·
68
+ [Login flow](https://github.com/alexkorolex/fastapi_keycloak_admin#login-flow) ·
69
+ [Endpoints](https://github.com/alexkorolex/fastapi_keycloak_admin#endpoints) ·
70
+ [Errors](https://github.com/alexkorolex/fastapi_keycloak_admin#errors) ·
71
+ [Configuration](https://github.com/alexkorolex/fastapi_keycloak_admin#configuration) ·
72
+ [Command line](https://github.com/alexkorolex/fastapi_keycloak_admin#command-line) ·
73
+ [Using the client](https://github.com/alexkorolex/fastapi_keycloak_admin#using-the-client-in-your-own-code) ·
74
+ [Public API](https://github.com/alexkorolex/fastapi_keycloak_admin#public-api) ·
75
+ [Security notes](https://github.com/alexkorolex/fastapi_keycloak_admin#security-notes)
76
+
77
+ ## Installation
78
+
79
+ Python 3.12 or newer and FastAPI 0.115 or newer are required.
80
+
81
+ ```bash
82
+ # pip
83
+ python -m pip install fastapi-keycloak-admin
84
+
85
+ # or uv
86
+ uv add fastapi-keycloak-admin
87
+ ```
88
+
89
+ It depends on `aiohttp`, `PyJWT` (with `cryptography`) and `click`; they are installed with it.
90
+
91
+ ## Quick start
92
+
93
+ ```python
94
+ from typing import Annotated
95
+
96
+ from fastapi import Depends, FastAPI
97
+ from fastapi_keycloak_admin import (
98
+ KeycloakAdmin,
99
+ KeycloakAdminConfig,
100
+ KeycloakUser,
101
+ MatchStrategy,
102
+ require_roles,
103
+ )
104
+
105
+ app = FastAPI()
106
+ KeycloakAdmin(KeycloakAdminConfig.from_env()).init_app(app)
107
+
108
+ analysts = require_roles("admin", "analyst", strategy=MatchStrategy.ANY)
109
+
110
+
111
+ @app.get("/reports")
112
+ async def reports(user: Annotated[KeycloakUser, Depends(analysts)]) -> dict[str, str]:
113
+ return {"requested_by": user.preferred_username or user.sub}
114
+ ```
115
+
116
+ With `KEYCLOAK_INTERNAL_URL`, `KEYCLOAK_REALM`, `KEYCLOAK_CLIENT_ID` and `KEYCLOAK_CLIENT_SECRET`
117
+ set, the application now has:
118
+
119
+ - `/reports` open to users holding `admin` **or** `analyst`;
120
+ - the auth endpoints under `/auth` (see [Endpoints](https://github.com/alexkorolex/fastapi_keycloak_admin#endpoints));
121
+ - an *Authorize* button in `/docs` that takes an access token.
122
+
123
+ A minimal local environment looks like this:
124
+
125
+ ```dotenv
126
+ KEYCLOAK_INTERNAL_URL=http://localhost:8080
127
+ KEYCLOAK_REALM=my-realm
128
+ KEYCLOAK_CLIENT_ID=my-backend
129
+ KEYCLOAK_CLIENT_SECRET=replace-me
130
+
131
+ # Optional: use a master-realm administrator for local development.
132
+ KEYCLOAK_ADMIN=admin
133
+ KEYCLOAK_ADMIN_PASSWORD=replace-me-too
134
+ ```
135
+
136
+ Keep secrets outside source control. In production, prefer a narrowly scoped service account
137
+ as described below.
138
+
139
+ ## Keycloak setup
140
+
141
+ The package expects:
142
+
143
+ 1. **A confidential client** (Client authentication *On*) with *Direct access grants* enabled.
144
+ This is the client the backend uses for the password grant. Its ID and secret go into
145
+ `KEYCLOAK_CLIENT_ID` / `KEYCLOAK_CLIENT_SECRET`.
146
+ 2. **An audience mapper** on that client, so access tokens carry its ID in `aud`
147
+ (*Client scopes → dedicated scope → Add mapper → Audience*). Tokens whose `azp` is the client
148
+ are accepted without it, unless `strict_audience` is set.
149
+ 3. **Realm roles** for your application, e.g. `admin`, `user`, `dispatcher`. They are not listed
150
+ anywhere in code: define them in Keycloak (or in the realm export you import) and they can be
151
+ assigned and checked right away. One realm role, `admin` by default, is the *admin role*: its
152
+ holders may list roles and register users.
153
+ 4. **Admin REST API access**, in one of two ways:
154
+ - set `KEYCLOAK_ADMIN` / `KEYCLOAK_ADMIN_PASSWORD` to a master-realm administrator (simplest,
155
+ fine for development); or
156
+ - leave them unset and enable *Service accounts* on the client, then give its service account
157
+ the `realm-management` roles `manage-users` and `view-realm` (recommended in production: the
158
+ backend gets only the rights it needs, in one realm).
159
+
160
+ A minimal realm export with both roles and the client:
161
+
162
+ ```json
163
+ {
164
+ "realm": "my-realm",
165
+ "roles": {
166
+ "realm": [
167
+ { "name": "admin", "description": "Full administrative access" },
168
+ { "name": "user", "description": "Regular user" }
169
+ ]
170
+ },
171
+ "clients": [
172
+ {
173
+ "clientId": "my-backend",
174
+ "publicClient": false,
175
+ "secret": "${KEYCLOAK_CLIENT_SECRET}",
176
+ "directAccessGrantsEnabled": true,
177
+ "standardFlowEnabled": false,
178
+ "protocolMappers": [
179
+ {
180
+ "name": "audience",
181
+ "protocol": "openid-connect",
182
+ "protocolMapper": "oidc-audience-mapper",
183
+ "config": { "included.client.audience": "my-backend", "access.token.claim": "true" }
184
+ }
185
+ ]
186
+ }
187
+ ]
188
+ }
189
+ ```
190
+
191
+ ## Protecting routes
192
+
193
+ Routes are public unless they depend on the caller. There is no middleware: authentication is a
194
+ FastAPI dependency, so it shows in the OpenAPI schema and applies exactly where it is declared.
195
+
196
+ Take the caller as a parameter with `CurrentUser`:
197
+
198
+ ```python
199
+ from fastapi_keycloak_admin import CurrentUser
200
+
201
+
202
+ @app.get("/profile")
203
+ async def profile(user: CurrentUser) -> dict[str, str]:
204
+ return {"id": user.sub, "email": user.email or ""}
205
+ ```
206
+
207
+ It is a `KeycloakUser` with `sub`, `preferred_username`, `email`, `given_name`, `family_name`,
208
+ `realm_roles`, `client_roles`, `scopes` and every raw claim in `claims`. Without a valid token
209
+ the request fails with `401` before the route runs.
210
+
211
+ Restrict a route to certain roles with one of the checks. Each is a dependency that returns the
212
+ caller, so it can be a parameter or sit in `dependencies=[...]` of a route or a whole router:
213
+
214
+ ```python
215
+ from fastapi import APIRouter, Depends
216
+ from fastapi_keycloak_admin import (
217
+ MatchStrategy,
218
+ require_client_roles,
219
+ require_roles,
220
+ requires_admin,
221
+ )
222
+
223
+ require_roles("admin", "dispatcher", strategy=MatchStrategy.ANY) # holds at least one of them
224
+ require_roles("dispatcher", "supervisor") # holds every one of them
225
+ require_client_roles("billing", "read") # a role of the client "billing"
226
+ requires_admin # holds the configured admin role
227
+
228
+ staff = APIRouter(dependencies=[Depends(require_roles("staff"))])
229
+ ```
230
+
231
+ > **Note:** `require_roles` defaults to `MatchStrategy.ALL`. For "any of these roles", which is
232
+ > what most role checks mean, pass `strategy=MatchStrategy.ANY` explicitly.
233
+
234
+ A caller lacking the roles gets `403`. FastAPI caches dependencies per request, so the token is
235
+ validated once however many checks a route has.
236
+
237
+ ## Login flow
238
+
239
+ The endpoints are designed for a browser frontend with its own login form:
240
+
241
+ 1. **Log in**: `POST /auth/login` with `{"username", "password"}`. The response holds the access
242
+ token. The refresh token is set as an `httponly` cookie and is never visible to JavaScript.
243
+ 2. **First login of a new user**: users created by an admin have a temporary password, so the
244
+ login responds `403` with the reason `passwordChangeRequired`
245
+ (see [Errors](https://github.com/alexkorolex/fastapi_keycloak_admin#errors)). The frontend
246
+ asks for a new password and calls `POST /auth/initial-password` with
247
+ `{"username", "password", "new_password"}`. On success it returns tokens, just like a login.
248
+ 3. **Calling the API**: send `Authorization: Bearer <token>`.
249
+ 4. **Keeping the session alive**: before the access token expires (`expires_in`, in seconds), call
250
+ `POST /auth/refresh`, or call it when an API request fails with the reason `expired`. The
251
+ browser sends the cookie on its own; no body is needed. A `401` from `/refresh` itself means
252
+ the Keycloak session is over and the user has to log in again.
253
+ 5. **Logging out**: `POST /auth/logout` ends the Keycloak session and clears the cookie. Dropping
254
+ the access token in the browser alone would leave the session usable until it expires.
255
+
256
+ For non-browser clients (mobile apps, scripts), disable the cookie with
257
+ `refresh_cookie=RefreshCookieConfig(enabled=False)`: the refresh token is then returned in the
258
+ response body and sent back as `{"refresh_token": "..."}` to `/refresh` and `/logout`.
259
+
260
+ ## Login verification by code
261
+
262
+ Set `login_verification` to require a one-time code after the password. The application
263
+ decides how the code reaches the user - the package only calls `send_code`:
264
+
265
+ ```python
266
+ from fastapi_keycloak_admin import KeycloakAdminConfig, LoginCode, LoginVerificationConfig
267
+
268
+
269
+ async def send_code(code: LoginCode) -> None:
270
+ await mailer.send(code.email, f"Your login code: {code.code}") # or publish to a queue
271
+
272
+
273
+ config = KeycloakAdminConfig.from_env(
274
+ login_verification=LoginVerificationConfig(send_code=send_code),
275
+ )
276
+ ```
277
+
278
+ `POST /login` and `POST /initial-password` then check the password and answer
279
+ `202 {"challenge_id", "expires_in", "destination": "a***@example.com", "code_required": true}`
280
+ instead of tokens. `POST /login/verify` with the right code returns the usual `TokenResponse`
281
+ (and the refresh cookie); `POST /login/resend` sends a new code for the same challenge.
282
+
283
+ - The Keycloak tokens of the checked password wait in `LoginVerificationConfig.store` and are
284
+ refreshed on success. The default `MemoryChallengeStore` lives in one process; when the app
285
+ runs several workers, pass a shared store:
286
+
287
+ ```python
288
+ from redis.asyncio import Redis
289
+ from fastapi_keycloak_admin import LoginVerificationConfig, RedisChallengeStore
290
+
291
+ LoginVerificationConfig(send_code=send_code, store=RedisChallengeStore(Redis.from_url(url)))
292
+ ```
293
+
294
+ Any object with async `get`, `set(key, value, expires_in)` and `delete` (`ChallengeStore`)
295
+ works as well.
296
+ - The code is kept only as an HMAC; `max_attempts` wrong codes (default 5) end the Keycloak
297
+ session. Codes live `ttl_seconds` (300); `resend_interval_seconds` (60) and `max_sends` (4)
298
+ limit resending.
299
+ - The e-mail comes from the access token's `email` claim; an account without one gets
300
+ `403 emailRequired`. A `send_code` that raises gives `503 codeDeliveryFailed`.
301
+ - Error reasons: `invalidCode` (401), `challengeExpired` (401), `tooManyAttempts` (429),
302
+ `tooManySends` (429), `resendTooSoon` (429 with `Retry-After`).
303
+
304
+ ### Letting users turn the code off
305
+
306
+ With `LoginVerificationConfig(user_choice=True)` every user decides for themselves:
307
+
308
+ | Method and path | Body | Success |
309
+ | --- | --- | --- |
310
+ | `GET /me/login-verification` | | `200`, `{"enabled": bool, "changeable": bool}` |
311
+ | `PUT /me/login-verification` | `enabled`, `password` (the current one) | `200`, same body |
312
+
313
+ The choice lives in the user attribute `user_attribute` (default `login_verification`,
314
+ `off` skips the code), written through the Admin API with the other attributes kept - the
315
+ realm must allow it (User Profile `unmanagedAttributePolicy` `ADMIN_EDIT` or a declared
316
+ attribute). The code stays required for everyone who has not turned it off and whenever the
317
+ attribute cannot be read. Without `user_choice` the `PUT` answers `404`.
318
+
319
+ ## Endpoints
320
+
321
+ All paths are relative to `auth_path` (default `/auth`).
322
+
323
+ ### Public (no token)
324
+
325
+ | Method and path | Body | Success |
326
+ | --- | --- | --- |
327
+ | `POST /login` | `username`, `password` | `200`, `TokenResponse` |
328
+ | `POST /initial-password` | `username`, `password` (the temporary one), `new_password` | `200`, `TokenResponse` |
329
+ | `POST /refresh` | `refresh_token` (only without the cookie) | `200`, `TokenResponse` |
330
+ | `POST /logout` | `refresh_token` (only without the cookie) | `204` |
331
+ | `POST /login/verify` | `challenge_id`, `code` | `200`, `TokenResponse` |
332
+ | `POST /login/resend` | `challenge_id` | `202`, `LoginChallengeResponse` |
333
+
334
+ The last two exist only with `login_verification`; otherwise they answer `404`.
335
+
336
+ `TokenResponse` is
337
+ `{"token": str, "expires_in": int, "refresh_expires_in": int, "refresh_token": str | null}`;
338
+ `refresh_token` is `null` while the cookie is enabled.
339
+
340
+ ### Any authenticated user
341
+
342
+ | Method and path | Body | Success |
343
+ | --- | --- | --- |
344
+ | `GET /me` | | `200`, `UserResponse` |
345
+ | `PATCH /me` | any of `first_name`, `last_name`, `email` (`""` removes the e-mail) | `200`, `UserResponse` |
346
+ | `POST /password` | `current_password`, `new_password` | `204` |
347
+
348
+ `UserResponse` is
349
+ `{"id", "username", "email", "first_name", "last_name", "roles": [str]}`. It is built from the
350
+ access token, so after `PATCH /me` the token itself keeps the old values until it is refreshed.
351
+
352
+ ### Admin role only
353
+
354
+ | Method and path | Body | Success |
355
+ | --- | --- | --- |
356
+ | `GET /roles` | | `200`, `[{"name", "description"}]` |
357
+ | `POST /register` | `username`, `password`, `roles: [str]`, optional `email`, `first_name`, `last_name` | `201`, `UserResponse` |
358
+
359
+ `GET /roles` returns the realm's own roles and leaves out Keycloak's built-in ones
360
+ (`offline_access`, `uma_authorization`, `default-roles-<realm>`). Use it to fill a role picker.
361
+
362
+ `POST /register` creates the user with a **temporary** password and the given roles, at least
363
+ one. It fails with `400`, and creates nothing, if a role doesn't exist in the realm or isn't in
364
+ `assignable_roles`.
365
+
366
+ To mount the endpoints yourself (another prefix, extra dependencies), call
367
+ `init_app(app, include_router=False)` and include `fastapi_keycloak_admin.routes.router`; set
368
+ `refresh_cookie.path` to the new prefix so the browser sends the cookie back.
369
+
370
+ ## Errors
371
+
372
+ Errors of the built-in endpoints, of tokens and of the role checks have the same shape:
373
+
374
+ ```json
375
+ {
376
+ "error": {
377
+ "errors": [
378
+ {
379
+ "domain": "global",
380
+ "reason": "invalidParameter",
381
+ "message": "Password must be at least 8 characters long",
382
+ "locationType": "body",
383
+ "location": "password"
384
+ }
385
+ ],
386
+ "code": 400,
387
+ "message": "Password must be at least 8 characters long"
388
+ }
389
+ }
390
+ ```
391
+
392
+ - `code` repeats the HTTP status, and `message` is meant for humans.
393
+ - `errors[].reason` is the stable, machine-readable part: **branch on it, not on the message**.
394
+ - `location` and `locationType` say what the error is about, when that is known: a body field
395
+ (`body`), a query or path parameter (`query`, `path`), or a header (`header`, e.g.
396
+ `Authorization`).
397
+
398
+ | Status | `reason` | When |
399
+ | --- | --- | --- |
400
+ | `400` | `invalidParameter` | A field is missing, has the wrong type or fails a check (password too short, no roles); one item per field |
401
+ | `400` | `invalid` | Keycloak refused the input: role missing from the realm, password breaks the realm's policy |
402
+ | `400` | `badRequest` | Malformed request, e.g. invalid JSON |
403
+ | `401` | `required` | No access token, or no session to refresh |
404
+ | `401` | `expired` | The access token has expired: refresh it |
405
+ | `401` | `authError` | Invalid token, wrong credentials, expired or ended session |
406
+ | `403` | `passwordChangeRequired` | Login with a temporary password: call `/initial-password` |
407
+ | `403` | `insufficientPermissions` | The caller lacks a required role |
408
+ | `403` | `insufficientScope` | The token lacks a required scope |
409
+ | `409` | `conflict` | User or e-mail already exists; `/initial-password` for an account without a temporary password |
410
+ | `500` | `internalError` | Unexpected server error; no details are exposed |
411
+ | `502` | `backendError` | Keycloak answered with something unexpected |
412
+ | `503` | `backendError` | Keycloak is unreachable |
413
+
414
+ Token errors also set `WWW-Authenticate: Bearer`. Validation errors use `400`, not FastAPI's
415
+ usual `422`, on the built-in endpoints.
416
+
417
+ `init_app` registers handlers for `KeycloakClientError`, `KeycloakTokenError` and `ApiError`,
418
+ unless the app already has its own for them. The rest of the application keeps FastAPI's
419
+ format (`{"detail": ...}`, `422`). To use this format everywhere, including FastAPI's own 404,
420
+ 405 and validation errors and unhandled exceptions:
421
+
422
+ ```python
423
+ from fastapi_keycloak_admin import install_error_handlers
424
+
425
+ install_error_handlers(app)
426
+ ```
427
+
428
+ Or only for some routers: `APIRouter(route_class=KeycloakRoute)`. In your own routes,
429
+ `raise ApiError(status, message, [ErrorItem(reason=..., message=...)])` answers in this format
430
+ from anywhere, and `error_response(...)` builds a response directly.
431
+
432
+ ## Configuration
433
+
434
+ `KeycloakAdminConfig.from_env()` reads these variables (prefix `KEYCLOAK_`, change it with
435
+ `from_env(prefix=...)`):
436
+
437
+ | Variable | Required | Meaning |
438
+ | --- | --- | --- |
439
+ | `INTERNAL_URL` | yes | Where the backend reaches Keycloak, e.g. `http://keycloak:8080` |
440
+ | `REALM` | yes | Realm name |
441
+ | `CLIENT_ID` | yes | The confidential client (see [Keycloak setup](https://github.com/alexkorolex/fastapi_keycloak_admin#keycloak-setup)) |
442
+ | `CLIENT_SECRET` | yes | Its secret |
443
+ | `ISSUER` | no | Expected `iss`: the URL users reach Keycloak at, plus `/realms/<realm>`. Needed when it differs from `INTERNAL_URL`, e.g. behind a reverse proxy |
444
+ | `AUDIENCE` | no | Expected `aud`; defaults to `CLIENT_ID` |
445
+ | `ADMIN`, `ADMIN_PASSWORD` | no | Master-realm administrator for the Admin REST API; without them the client's service account is used |
446
+ | `REFRESH_COOKIE_PATH` | no | Cookie path when a reverse proxy serves the API under a prefix, e.g. `/api/auth`; otherwise the browser never sends the cookie back |
447
+
448
+ Keyword arguments refine the result:
449
+
450
+ - `keycloak={...}` passes extra arguments to `KeycloakConfig`:
451
+
452
+ | Field | Default | Meaning |
453
+ | --- | --- | --- |
454
+ | `optional_audiences` | `frozenset()` | More audiences to accept, e.g. other clients calling the API |
455
+ | `strict_audience` | `False` | Only `aud` counts; by default a token whose `azp` is accepted passes too |
456
+ | `expected_token_type` | `"Bearer"` | Required `typ`; rejects ID and refresh tokens. `None` skips the check |
457
+ | `algorithms` | `("RS256",)` | Accepted signing algorithms |
458
+ | `jwks_cache_ttl` | `3600` | Seconds the signing keys are cached |
459
+
460
+ - other arguments override `KeycloakAdminConfig` fields:
461
+
462
+ | Field | Default | Meaning |
463
+ | --- | --- | --- |
464
+ | `admin_role` | `"admin"` | Realm role allowed to list roles and register users |
465
+ | `assignable_roles` | `()` | Roles registration may assign; empty means every realm role |
466
+ | `auth_path` | `"/auth"` | Where the endpoints are mounted |
467
+ | `refresh_cookie` | `RefreshCookieConfig()` | `enabled`, `name` (`kc_refresh`), `path`, `secure`, `samesite` (`strict`) |
468
+ | `min_password_length` | `8` | Checked before Keycloak's own password policy |
469
+ | `timeout` | `10` | Seconds per request to Keycloak |
470
+ | `login_verification` | `None` | See [Login verification by code](https://github.com/alexkorolex/fastapi_keycloak_admin#login-verification-by-code) |
471
+
472
+ `KeycloakAdminConfig(keycloak=KeycloakConfig(...), ...)` can also be built directly, without
473
+ environment variables.
474
+
475
+ ## Command line
476
+
477
+ The package installs a `fastapi-keycloak-admin` command. Its main use is creating the first
478
+ administrator, when nobody can log in to `/register` yet. It reads the same `KEYCLOAK_*`
479
+ variables as `from_env`:
480
+
481
+ ```bash
482
+ KEYCLOAK_NEW_USER_PASSWORD='temporary-secret' \
483
+ fastapi-keycloak-admin create-user root --role admin --exist-ok
484
+ ```
485
+
486
+ - `--role` may be repeated to assign several roles;
487
+ - the password comes from `KEYCLOAK_NEW_USER_PASSWORD` or an interactive prompt, never from
488
+ command-line arguments, which other processes can see;
489
+ - the password is temporary: the user replaces it at first login;
490
+ - `--exist-ok` exits successfully if the user already exists, so the command is safe to run on
491
+ every deployment;
492
+ - `fastapi-keycloak-admin --env-prefix MYAPP_KC_ create-user ...` reads other variables.
493
+
494
+ ## Using the client in your own code
495
+
496
+ `KeycloakAdminClient` is available to any route as the `KeycloakAdminDep` dependency, for
497
+ user-management needs beyond the built-in endpoints. For example, an administrator resetting a
498
+ user's password to a temporary one, which the user replaces at the next login:
499
+
500
+ ```python
501
+ from fastapi import Depends
502
+ from pydantic import BaseModel
503
+ from fastapi_keycloak_admin import ApiError, KeycloakAdminDep, requires_admin
504
+
505
+
506
+ class ResetPasswordRequest(BaseModel):
507
+ password: str
508
+
509
+
510
+ @app.post(
511
+ "/users/{username}/reset-password",
512
+ status_code=204,
513
+ dependencies=[Depends(requires_admin)],
514
+ )
515
+ async def reset_password(
516
+ username: str, data: ResetPasswordRequest, keycloak_admin: KeycloakAdminDep
517
+ ) -> None:
518
+ subject = await keycloak_admin.find_user_id(username)
519
+ if subject is None:
520
+ raise ApiError(404, f"User {username!r} not found")
521
+ await keycloak_admin.set_password(subject, data.password, temporary=True)
522
+ ```
523
+
524
+ | Method | Does |
525
+ | --- | --- |
526
+ | `login(username, password)` | Password grant; returns Keycloak's token response |
527
+ | `refresh(refresh_token)` | New tokens for a live session |
528
+ | `logout(refresh_token)` | Ends the session |
529
+ | `check_password(username, password)` | Whether the password is the user's current one |
530
+ | `complete_initial_password(username, password, new_password)` | Replaces a temporary password and logs in |
531
+ | `list_roles()` | The realm's own roles, without Keycloak's built-in ones |
532
+ | `create_user(username=..., password=..., roles=..., ...)` | Creates a user, returns its subject id |
533
+ | `update_user(subject, **representation)` | Partial update with Keycloak's `UserRepresentation` field names |
534
+ | `set_password(subject, password, temporary=False)` | Sets a password |
535
+ | `get_user(subject)` | The full `UserRepresentation`, or `None` |
536
+ | `set_user_attribute(subject, name, values)` | Replaces one user attribute, keeps the others |
537
+ | `find_user_id(username)` | Subject id for an exact username, or `None` |
538
+
539
+ Its errors (`KeycloakClientError` and subclasses) are turned into the HTTP responses listed in
540
+ [Errors](https://github.com/alexkorolex/fastapi_keycloak_admin#errors) wherever they are raised.
541
+
542
+ ## Public API
543
+
544
+ Everything below is importable from `fastapi_keycloak_admin`; other modules are internal,
545
+ except `fastapi_keycloak_admin.routes.router`.
546
+
547
+ | Name | Kind |
548
+ | --- | --- |
549
+ | `KeycloakAdmin` | Installs everything on the application with `init_app(app)` |
550
+ | `KeycloakAdminConfig`, `KeycloakConfig`, `RefreshCookieConfig` | Configuration |
551
+ | `CurrentUser`, `KeycloakUser`, `get_current_user` | The authenticated caller |
552
+ | `require_roles`, `require_client_roles`, `require_scopes`, `requires_admin`, `MatchStrategy` | Access checks |
553
+ | `KeycloakAdminClient`, `KeycloakAdminDep`, `get_keycloak_admin` | Asynchronous client for Keycloak's token endpoints and Admin REST API |
554
+ | `LoginVerificationConfig`, `LoginCode`, `LoginCodeSender` | Login verification by code |
555
+ | `ChallengeStore`, `MemoryChallengeStore`, `RedisChallengeStore` | Where pending challenges wait |
556
+ | `UserResponse`, `LoginChallengeResponse`, `LoginVerificationSettings` | Response models of the built-in endpoints |
557
+ | `ApiError`, `ErrorItem`, `error_response`, `install_error_handlers`, `KeycloakRoute` | The error format |
558
+ | `KeycloakClientError`, `KeycloakUnavailableError`, `KeycloakLoginError`, `PasswordChangeRequiredError`, `KeycloakAdminError`, `LoginChallengeError` | Client exceptions |
559
+ | `KeycloakTokenError`, `AuthenticationError`, `MissingTokenError`, `TokenExpiredError`, `InvalidTokenError`, `AuthorizationError`, `InsufficientRoleError`, `InsufficientScopeError`, `JWKSFetchError` | Token and access exceptions |
560
+
561
+ The package follows [Semantic Versioning](https://semver.org/); while it is in `0.x`, a minor
562
+ release may contain breaking changes, which are listed in the
563
+ [changelog](https://github.com/alexkorolex/fastapi_keycloak_admin/blob/main/CHANGELOG.md).
564
+
565
+ ## Security notes
566
+
567
+ - **Password grant.** The login endpoints use OAuth's *Resource Owner Password Credentials*
568
+ grant: the password passes through your backend to Keycloak and is never stored. It is what
569
+ allows a custom login form, but OAuth 2.1 deprecates it, and it rules out Keycloak features
570
+ that need its own login page (social login, WebAuthn, its two-factor flows). If you need them,
571
+ use the Authorization Code flow in the frontend; `CurrentUser` and the role checks work with
572
+ those tokens just the same.
573
+ - **Token validation.** Tokens are checked locally against the realm's JWKS: signature,
574
+ `exp`, `typ`, `iss` and `aud`/`azp`. A revoked session's access token stays valid until it
575
+ expires, so keep Keycloak's access token lifespan short. Signing keys are refetched after
576
+ `jwks_cache_ttl`, or at most every 10 seconds for a token signed with an unknown key.
577
+ - **Refresh token.** By default it stays in an `httponly`, `secure`, `SameSite=Strict` cookie
578
+ scoped to `auth_path`, out of reach of JavaScript. `secure` cookies need HTTPS; disable it only
579
+ for local development.
580
+ - **Admin credentials.** Prefer a service account with `manage-users` and `view-realm` to a
581
+ master-realm administrator in production.
582
+
583
+ ## Development
584
+
585
+ ```bash
586
+ uv sync --group dev
587
+ uv run ruff check .
588
+ uv run ruff format --check .
589
+ uv run pytest
590
+ uv build
591
+ uv run twine check --strict dist/*
592
+ ```
593
+
594
+ The tests start a fake Keycloak on `aiohttp.web` (`tests/fake_keycloak.py`), so they run real
595
+ HTTP against the admin client and token validation without a Keycloak instance.
596
+
597
+ See [CONTRIBUTING.md](https://github.com/alexkorolex/fastapi_keycloak_admin/blob/main/CONTRIBUTING.md)
598
+ for contribution guidelines,
599
+ [CHANGELOG.md](https://github.com/alexkorolex/fastapi_keycloak_admin/blob/main/CHANGELOG.md)
600
+ for user-visible changes, and the
601
+ [release guide](https://github.com/alexkorolex/fastapi_keycloak_admin/blob/main/docs/releasing.md)
602
+ for the release process.
603
+
604
+ ## License
605
+
606
+ Distributed under the
607
+ [MIT License](https://github.com/alexkorolex/fastapi_keycloak_admin/blob/main/LICENSE).