litestar-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.
- litestar_keycloak_admin-0.1.0/LICENSE +21 -0
- litestar_keycloak_admin-0.1.0/PKG-INFO +510 -0
- litestar_keycloak_admin-0.1.0/README.md +478 -0
- litestar_keycloak_admin-0.1.0/pyproject.toml +74 -0
- litestar_keycloak_admin-0.1.0/pyproject.toml.orig +67 -0
- litestar_keycloak_admin-0.1.0/src/litestar_keycloak_admin/__init__.py +42 -0
- litestar_keycloak_admin-0.1.0/src/litestar_keycloak_admin/client.py +329 -0
- litestar_keycloak_admin-0.1.0/src/litestar_keycloak_admin/config.py +119 -0
- litestar_keycloak_admin-0.1.0/src/litestar_keycloak_admin/controller.py +313 -0
- litestar_keycloak_admin-0.1.0/src/litestar_keycloak_admin/errors.py +198 -0
- litestar_keycloak_admin-0.1.0/src/litestar_keycloak_admin/exceptions.py +31 -0
- litestar_keycloak_admin-0.1.0/src/litestar_keycloak_admin/plugin.py +142 -0
- litestar_keycloak_admin-0.1.0/src/litestar_keycloak_admin/py.typed +0 -0
|
@@ -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,510 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: litestar-keycloak-admin
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Keycloak account and user administration for Litestar applications
|
|
5
|
+
Keywords: authentication,keycloak,litestar,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: Intended Audience :: Developers
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
20
|
+
Classifier: Topic :: Security
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Dist: aiohttp>=3.9
|
|
23
|
+
Requires-Dist: click>=8.1
|
|
24
|
+
Requires-Dist: litestar>=2.24.0
|
|
25
|
+
Requires-Dist: litestar-keycloak>=0.3.2,<0.4
|
|
26
|
+
Requires-Python: >=3.12
|
|
27
|
+
Project-URL: Changelog, https://github.com/alexkorolex/litestar_keycloak_admin/blob/main/CHANGELOG.md
|
|
28
|
+
Project-URL: Documentation, https://github.com/alexkorolex/litestar_keycloak_admin#readme
|
|
29
|
+
Project-URL: Issues, https://github.com/alexkorolex/litestar_keycloak_admin/issues
|
|
30
|
+
Project-URL: Repository, https://github.com/alexkorolex/litestar_keycloak_admin
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# litestar-keycloak-admin
|
|
34
|
+
|
|
35
|
+
[](https://pypi.org/project/litestar-keycloak-admin/)
|
|
36
|
+
[](https://pypi.org/project/litestar-keycloak-admin/)
|
|
37
|
+
[](https://github.com/alexkorolex/litestar_keycloak_admin/actions/workflows/ci.yml)
|
|
38
|
+
[](https://github.com/alexkorolex/litestar_keycloak_admin/blob/main/LICENSE)
|
|
39
|
+
|
|
40
|
+
Account and user management for Litestar applications that use Keycloak as their identity
|
|
41
|
+
provider. It lets your frontend keep its own login form, lets users manage their own account,
|
|
42
|
+
and lets administrators create users with any realm roles. Neither the browser nor your code
|
|
43
|
+
ever has to talk to Keycloak directly.
|
|
44
|
+
|
|
45
|
+
> The project is currently alpha software. Pin the version, test upgrades in a staging
|
|
46
|
+
> environment, and review the [security notes](https://github.com/alexkorolex/litestar_keycloak_admin#security-notes) before using it in production.
|
|
47
|
+
|
|
48
|
+
The package is built on top of [litestar-keycloak](https://github.com/smirnoffmg/litestar-keycloak)
|
|
49
|
+
and complements it rather than replacing it:
|
|
50
|
+
|
|
51
|
+
| Concern | Handled by |
|
|
52
|
+
| --- | --- |
|
|
53
|
+
| Validating access tokens (JWKS, `iss`, `aud`, `exp`) | `litestar-keycloak` |
|
|
54
|
+
| `current_user` injection, `require_roles` / `require_scopes` guards | `litestar-keycloak` |
|
|
55
|
+
| Login with username and password, refresh, logout | **this package** |
|
|
56
|
+
| Replacing a temporary password at first login | **this package** |
|
|
57
|
+
| Own profile and password | **this package** |
|
|
58
|
+
| Listing realm roles, registering users | **this package** |
|
|
59
|
+
| Creating users from the command line (e.g. the first admin) | **this package** |
|
|
60
|
+
|
|
61
|
+
`KeycloakAdminPlugin` installs `litestar-keycloak`'s `KeycloakPlugin` for you, so an application
|
|
62
|
+
registers exactly one plugin.
|
|
63
|
+
|
|
64
|
+
**Contents:**
|
|
65
|
+
[Installation](https://github.com/alexkorolex/litestar_keycloak_admin#installation) ·
|
|
66
|
+
[Quick start](https://github.com/alexkorolex/litestar_keycloak_admin#quick-start) ·
|
|
67
|
+
[Keycloak setup](https://github.com/alexkorolex/litestar_keycloak_admin#keycloak-setup) ·
|
|
68
|
+
[Protecting routes](https://github.com/alexkorolex/litestar_keycloak_admin#protecting-routes) ·
|
|
69
|
+
[Login flow](https://github.com/alexkorolex/litestar_keycloak_admin#login-flow) ·
|
|
70
|
+
[Endpoints](https://github.com/alexkorolex/litestar_keycloak_admin#endpoints) ·
|
|
71
|
+
[Errors](https://github.com/alexkorolex/litestar_keycloak_admin#errors) ·
|
|
72
|
+
[Configuration](https://github.com/alexkorolex/litestar_keycloak_admin#configuration) ·
|
|
73
|
+
[Command line](https://github.com/alexkorolex/litestar_keycloak_admin#command-line) ·
|
|
74
|
+
[Using the client](https://github.com/alexkorolex/litestar_keycloak_admin#using-the-client-in-your-own-code) ·
|
|
75
|
+
[Public API](https://github.com/alexkorolex/litestar_keycloak_admin#public-api) ·
|
|
76
|
+
[Security notes](https://github.com/alexkorolex/litestar_keycloak_admin#security-notes)
|
|
77
|
+
|
|
78
|
+
## Installation
|
|
79
|
+
|
|
80
|
+
Python 3.12 or newer is required.
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
# pip
|
|
84
|
+
python -m pip install litestar-keycloak-admin
|
|
85
|
+
|
|
86
|
+
# or uv
|
|
87
|
+
uv add litestar-keycloak-admin
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
It depends on `litestar-keycloak` (0.3.x) and `aiohttp`; both are installed with it.
|
|
91
|
+
|
|
92
|
+
## Quick start
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
from litestar import Litestar, get
|
|
96
|
+
from litestar_keycloak import CurrentUser, MatchStrategy, require_roles
|
|
97
|
+
from litestar_keycloak_admin import KeycloakAdminConfig, KeycloakAdminPlugin
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@get("/reports", guards=[require_roles("admin", "analyst", strategy=MatchStrategy.ANY)])
|
|
101
|
+
async def reports(current_user: CurrentUser) -> dict[str, str]:
|
|
102
|
+
return {"requested_by": current_user.preferred_username or current_user.sub}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
app = Litestar(
|
|
106
|
+
route_handlers=[reports],
|
|
107
|
+
plugins=[
|
|
108
|
+
KeycloakAdminPlugin(
|
|
109
|
+
KeycloakAdminConfig.from_env(keycloak={"exclude_patterns": ("^/schema", "^/health$")}),
|
|
110
|
+
)
|
|
111
|
+
],
|
|
112
|
+
)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
With `KEYCLOAK_INTERNAL_URL`, `KEYCLOAK_REALM`, `KEYCLOAK_CLIENT_ID` and `KEYCLOAK_CLIENT_SECRET`
|
|
116
|
+
set, the application now has:
|
|
117
|
+
|
|
118
|
+
- every route protected by a Keycloak access token, except `/schema`, `/health` and the public
|
|
119
|
+
auth endpoints;
|
|
120
|
+
- `/reports` open to users holding `admin` **or** `analyst`;
|
|
121
|
+
- the auth endpoints under `/auth` (see [Endpoints](https://github.com/alexkorolex/litestar_keycloak_admin#endpoints)).
|
|
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*). Without it every token is rejected
|
|
148
|
+
as having the wrong audience.
|
|
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
|
+
Authentication is **on by default**: `litestar-keycloak` rejects any request without a valid
|
|
194
|
+
token with `401`. Make a route public in one of two ways:
|
|
195
|
+
|
|
196
|
+
- a path pattern in `exclude_patterns` (regular expressions, matched against the path):
|
|
197
|
+
`KeycloakAdminConfig.from_env(keycloak={"exclude_patterns": ("^/schema", "^/webhooks/")})`;
|
|
198
|
+
- a handler flag: `@get("/health", opt={"exclude_from_auth": True})`.
|
|
199
|
+
|
|
200
|
+
On a protected route, inject the caller as `current_user: CurrentUser` (the parameter must have
|
|
201
|
+
exactly this name). It is a `litestar_keycloak.KeycloakUser` with `sub`, `preferred_username`,
|
|
202
|
+
`email`, `given_name`, `family_name`, `realm_roles` and `client_roles`.
|
|
203
|
+
|
|
204
|
+
Restrict a route to certain roles with `require_roles`:
|
|
205
|
+
|
|
206
|
+
```python
|
|
207
|
+
from litestar_keycloak import MatchStrategy, require_roles
|
|
208
|
+
from litestar_keycloak_admin import requires_admin
|
|
209
|
+
|
|
210
|
+
require_roles("admin", "dispatcher", strategy=MatchStrategy.ANY) # holds at least one of them
|
|
211
|
+
require_roles("dispatcher", "supervisor") # holds every one of them
|
|
212
|
+
requires_admin # holds the configured admin role
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
> **Note:** `require_roles` defaults to `MatchStrategy.ALL`. For "any of these roles", which is
|
|
216
|
+
> what most role checks mean, pass `strategy=MatchStrategy.ANY` explicitly.
|
|
217
|
+
|
|
218
|
+
A caller lacking the roles gets `403`.
|
|
219
|
+
|
|
220
|
+
## Login flow
|
|
221
|
+
|
|
222
|
+
The endpoints are designed for a browser frontend with its own login form:
|
|
223
|
+
|
|
224
|
+
1. **Log in**: `POST /auth/login` with `{"username", "password"}`. The response holds the access
|
|
225
|
+
token. The refresh token is set as an `httponly` cookie and is never visible to JavaScript.
|
|
226
|
+
2. **First login of a new user**: users created by an admin have a temporary password, so the
|
|
227
|
+
login responds `403` with the reason `passwordChangeRequired`
|
|
228
|
+
(see [Errors](https://github.com/alexkorolex/litestar_keycloak_admin#errors)). The frontend
|
|
229
|
+
asks for a new password and calls `POST /auth/initial-password` with
|
|
230
|
+
`{"username", "password", "new_password"}`. On success it returns tokens, just like a login.
|
|
231
|
+
3. **Calling the API**: send `Authorization: Bearer <token>`.
|
|
232
|
+
4. **Keeping the session alive**: before the access token expires (`expires_in`, in seconds), call
|
|
233
|
+
`POST /auth/refresh`, or call it when an API request fails with the reason `expired`. The
|
|
234
|
+
browser sends the cookie on its own; no body is needed. A `401` from `/refresh` itself means
|
|
235
|
+
the Keycloak session is over and the user has to log in again.
|
|
236
|
+
5. **Logging out**: `POST /auth/logout` ends the Keycloak session and clears the cookie. Dropping
|
|
237
|
+
the access token in the browser alone would leave the session usable until it expires.
|
|
238
|
+
|
|
239
|
+
For non-browser clients (mobile apps, scripts), disable the cookie with
|
|
240
|
+
`refresh_cookie=RefreshCookieConfig(enabled=False)`: the refresh token is then returned in the
|
|
241
|
+
response body and sent back as `{"refresh_token": "..."}` to `/refresh` and `/logout`.
|
|
242
|
+
|
|
243
|
+
## Endpoints
|
|
244
|
+
|
|
245
|
+
All paths are relative to `auth_path` (default `/auth`).
|
|
246
|
+
|
|
247
|
+
### Public (no token)
|
|
248
|
+
|
|
249
|
+
| Method and path | Body | Success |
|
|
250
|
+
| --- | --- | --- |
|
|
251
|
+
| `POST /login` | `username`, `password` | `200`, `TokenResponse` |
|
|
252
|
+
| `POST /initial-password` | `username`, `password` (the temporary one), `new_password` | `200`, `TokenResponse` |
|
|
253
|
+
| `POST /refresh` | `refresh_token` (only without the cookie) | `200`, `TokenResponse` |
|
|
254
|
+
| `POST /logout` | `refresh_token` (only without the cookie) | `204` |
|
|
255
|
+
|
|
256
|
+
`TokenResponse` is
|
|
257
|
+
`{"token": str, "expires_in": int, "refresh_expires_in": int, "refresh_token": str | null}`;
|
|
258
|
+
`refresh_token` is `null` while the cookie is enabled.
|
|
259
|
+
|
|
260
|
+
### Any authenticated user
|
|
261
|
+
|
|
262
|
+
| Method and path | Body | Success |
|
|
263
|
+
| --- | --- | --- |
|
|
264
|
+
| `GET /me` | | `200`, `UserResponse` |
|
|
265
|
+
| `PATCH /me` | any of `first_name`, `last_name`, `email` (`""` removes the e-mail) | `200`, `UserResponse` |
|
|
266
|
+
| `POST /password` | `current_password`, `new_password` | `204` |
|
|
267
|
+
|
|
268
|
+
`UserResponse` is
|
|
269
|
+
`{"id", "username", "email", "first_name", "last_name", "roles": [str]}`. It is built from the
|
|
270
|
+
access token, so after `PATCH /me` the token itself keeps the old values until it is refreshed.
|
|
271
|
+
|
|
272
|
+
### Admin role only
|
|
273
|
+
|
|
274
|
+
| Method and path | Body | Success |
|
|
275
|
+
| --- | --- | --- |
|
|
276
|
+
| `GET /roles` | | `200`, `[{"name", "description"}]` |
|
|
277
|
+
| `POST /register` | `username`, `password`, `roles: [str]`, optional `email`, `first_name`, `last_name` | `201`, `UserResponse` |
|
|
278
|
+
|
|
279
|
+
`GET /roles` returns the realm's own roles and leaves out Keycloak's built-in ones
|
|
280
|
+
(`offline_access`, `uma_authorization`, `default-roles-<realm>`). Use it to fill a role picker.
|
|
281
|
+
|
|
282
|
+
`POST /register` creates the user with a **temporary** password and the given roles, at least
|
|
283
|
+
one. It fails with `400`, and creates nothing, if a role doesn't exist in the realm or isn't in
|
|
284
|
+
`assignable_roles`.
|
|
285
|
+
|
|
286
|
+
## Errors
|
|
287
|
+
|
|
288
|
+
Every error, whether it comes from this package, from `litestar-keycloak` (tokens, roles) or from
|
|
289
|
+
Litestar itself (validation), has the same shape:
|
|
290
|
+
|
|
291
|
+
```json
|
|
292
|
+
{
|
|
293
|
+
"error": {
|
|
294
|
+
"errors": [
|
|
295
|
+
{
|
|
296
|
+
"domain": "global",
|
|
297
|
+
"reason": "invalidParameter",
|
|
298
|
+
"message": "Password must be at least 8 characters long",
|
|
299
|
+
"locationType": "body",
|
|
300
|
+
"location": "password"
|
|
301
|
+
}
|
|
302
|
+
],
|
|
303
|
+
"code": 400,
|
|
304
|
+
"message": "Password must be at least 8 characters long"
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
- `code` repeats the HTTP status, and `message` is meant for humans.
|
|
310
|
+
- `errors[].reason` is the stable, machine-readable part: **branch on it, not on the message**.
|
|
311
|
+
- `location` and `locationType` say what the error is about, when that is known: a body field
|
|
312
|
+
(`body`), a query or path parameter (`query`, `path`), or a header (`header`, e.g.
|
|
313
|
+
`Authorization`).
|
|
314
|
+
|
|
315
|
+
| Status | `reason` | When |
|
|
316
|
+
| --- | --- | --- |
|
|
317
|
+
| `400` | `invalidParameter` | A field is missing, has the wrong type or fails a check (password too short, no roles); one item per field |
|
|
318
|
+
| `400` | `invalid` | Keycloak refused the input: role missing from the realm, password breaks the realm's policy |
|
|
319
|
+
| `400` | `badRequest` | Malformed request, e.g. invalid JSON |
|
|
320
|
+
| `401` | `required` | No access token, or no session to refresh |
|
|
321
|
+
| `401` | `expired` | The access token has expired: refresh it |
|
|
322
|
+
| `401` | `authError` | Invalid token, wrong credentials, expired or ended session |
|
|
323
|
+
| `403` | `passwordChangeRequired` | Login with a temporary password: call `/initial-password` |
|
|
324
|
+
| `403` | `insufficientPermissions` | The caller lacks a required role |
|
|
325
|
+
| `403` | `insufficientScope` | The token lacks a required scope |
|
|
326
|
+
| `409` | `conflict` | User or e-mail already exists; `/initial-password` for an account without a temporary password |
|
|
327
|
+
| `500` | `internalError` | Unexpected server error; no details are exposed |
|
|
328
|
+
| `502` | `backendError` | Keycloak answered with something unexpected |
|
|
329
|
+
| `503` | `backendError` | Keycloak is unreachable |
|
|
330
|
+
|
|
331
|
+
Token errors also set `WWW-Authenticate: Bearer`.
|
|
332
|
+
|
|
333
|
+
The plugin applies this format to its own endpoints and to token and role errors. To use it for
|
|
334
|
+
the whole application, register the same handlers app-wide:
|
|
335
|
+
|
|
336
|
+
```python
|
|
337
|
+
from litestar import Litestar
|
|
338
|
+
from litestar.exceptions import HTTPException
|
|
339
|
+
from litestar.status_codes import HTTP_500_INTERNAL_SERVER_ERROR
|
|
340
|
+
from litestar_keycloak_admin import handle_http_exception, handle_internal_error
|
|
341
|
+
|
|
342
|
+
app = Litestar(
|
|
343
|
+
exception_handlers={
|
|
344
|
+
HTTPException: handle_http_exception,
|
|
345
|
+
HTTP_500_INTERNAL_SERVER_ERROR: handle_internal_error,
|
|
346
|
+
},
|
|
347
|
+
...,
|
|
348
|
+
)
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
In your own handlers, `HTTPException(..., extra={"reason": "..."})` sets the `reason`, and
|
|
352
|
+
`error_response(status, message, [ErrorItem(...)])` builds a response directly.
|
|
353
|
+
|
|
354
|
+
## Configuration
|
|
355
|
+
|
|
356
|
+
`KeycloakAdminConfig.from_env()` reads these variables (prefix `KEYCLOAK_`, change it with
|
|
357
|
+
`from_env(prefix=...)`):
|
|
358
|
+
|
|
359
|
+
| Variable | Required | Meaning |
|
|
360
|
+
| --- | --- | --- |
|
|
361
|
+
| `INTERNAL_URL` | yes | Where the backend reaches Keycloak, e.g. `http://keycloak:8080` |
|
|
362
|
+
| `REALM` | yes | Realm name |
|
|
363
|
+
| `CLIENT_ID` | yes | The confidential client (see [Keycloak setup](https://github.com/alexkorolex/litestar_keycloak_admin#keycloak-setup)) |
|
|
364
|
+
| `CLIENT_SECRET` | yes | Its secret |
|
|
365
|
+
| `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 |
|
|
366
|
+
| `AUDIENCE` | no | Expected `aud`; defaults to `CLIENT_ID` |
|
|
367
|
+
| `ADMIN`, `ADMIN_PASSWORD` | no | Master-realm administrator for the Admin REST API; without them the client's service account is used |
|
|
368
|
+
| `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 |
|
|
369
|
+
|
|
370
|
+
Keyword arguments refine the result:
|
|
371
|
+
|
|
372
|
+
- `keycloak={...}` passes extra arguments to `litestar_keycloak.KeycloakConfig`, e.g.
|
|
373
|
+
`exclude_patterns`, `jwks_cache_ttl`, `optional_audiences`;
|
|
374
|
+
- other arguments override `KeycloakAdminConfig` fields:
|
|
375
|
+
|
|
376
|
+
| Field | Default | Meaning |
|
|
377
|
+
| --- | --- | --- |
|
|
378
|
+
| `admin_role` | `"admin"` | Realm role allowed to list roles and register users |
|
|
379
|
+
| `assignable_roles` | `()` | Roles registration may assign; empty means every realm role |
|
|
380
|
+
| `auth_path` | `"/auth"` | Where the endpoints are mounted |
|
|
381
|
+
| `refresh_cookie` | `RefreshCookieConfig()` | `enabled`, `name` (`kc_refresh`), `path`, `secure`, `samesite` (`strict`) |
|
|
382
|
+
| `min_password_length` | `8` | Checked before Keycloak's own password policy |
|
|
383
|
+
| `timeout` | `10` | Seconds per request to Keycloak |
|
|
384
|
+
|
|
385
|
+
`KeycloakAdminConfig(keycloak=KeycloakConfig(...), ...)` can also be built directly, without
|
|
386
|
+
environment variables.
|
|
387
|
+
|
|
388
|
+
## Command line
|
|
389
|
+
|
|
390
|
+
The plugin adds a `keycloak` group to the `litestar` CLI. Its main use is creating the first
|
|
391
|
+
administrator, when nobody can log in to `/register` yet:
|
|
392
|
+
|
|
393
|
+
```bash
|
|
394
|
+
KEYCLOAK_NEW_USER_PASSWORD='temporary-secret' \
|
|
395
|
+
litestar keycloak create-user root --role admin --exist-ok
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
- `--role` may be repeated to assign several roles;
|
|
399
|
+
- the password comes from `KEYCLOAK_NEW_USER_PASSWORD` or an interactive prompt, never from
|
|
400
|
+
command-line arguments, which other processes can see;
|
|
401
|
+
- the password is temporary: the user replaces it at first login;
|
|
402
|
+
- `--exist-ok` exits successfully if the user already exists, so the command is safe to run on
|
|
403
|
+
every deployment.
|
|
404
|
+
|
|
405
|
+
## Using the client in your own code
|
|
406
|
+
|
|
407
|
+
`KeycloakAdminClient` is available to any handler as the `keycloak_admin` dependency, for
|
|
408
|
+
user-management needs beyond the built-in endpoints. For example, an administrator resetting a
|
|
409
|
+
user's password to a temporary one, which the user replaces at the next login:
|
|
410
|
+
|
|
411
|
+
```python
|
|
412
|
+
from dataclasses import dataclass
|
|
413
|
+
|
|
414
|
+
from litestar import post
|
|
415
|
+
from litestar.di import NamedDependency
|
|
416
|
+
from litestar.exceptions import NotFoundException
|
|
417
|
+
from litestar.params import FromPath
|
|
418
|
+
from litestar_keycloak_admin import KeycloakAdminClient, requires_admin
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
@dataclass
|
|
422
|
+
class ResetPasswordRequest:
|
|
423
|
+
password: str
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
@post("/users/{username:str}/reset-password", guards=[requires_admin], status_code=204)
|
|
427
|
+
async def reset_password(
|
|
428
|
+
username: FromPath[str],
|
|
429
|
+
data: ResetPasswordRequest,
|
|
430
|
+
keycloak_admin: NamedDependency[KeycloakAdminClient],
|
|
431
|
+
) -> None:
|
|
432
|
+
subject = await keycloak_admin.find_user_id(username)
|
|
433
|
+
if subject is None:
|
|
434
|
+
raise NotFoundException(f"User {username!r} not found")
|
|
435
|
+
await keycloak_admin.set_password(subject, data.password, temporary=True)
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
| Method | Does |
|
|
439
|
+
| --- | --- |
|
|
440
|
+
| `login(username, password)` | Password grant; returns Keycloak's token response |
|
|
441
|
+
| `refresh(refresh_token)` | New tokens for a live session |
|
|
442
|
+
| `logout(refresh_token)` | Ends the session |
|
|
443
|
+
| `check_password(username, password)` | Whether the password is the user's current one |
|
|
444
|
+
| `complete_initial_password(username, password, new_password)` | Replaces a temporary password and logs in |
|
|
445
|
+
| `list_roles()` | The realm's own roles, without Keycloak's built-in ones |
|
|
446
|
+
| `create_user(username=..., password=..., roles=..., ...)` | Creates a user, returns its subject id |
|
|
447
|
+
| `update_user(subject, **representation)` | Partial update with Keycloak's `UserRepresentation` field names |
|
|
448
|
+
| `set_password(subject, password, temporary=False)` | Sets a password |
|
|
449
|
+
| `find_user_id(username)` | Subject id for an exact username, or `None` |
|
|
450
|
+
|
|
451
|
+
Its errors (`KeycloakClientError` and subclasses) are turned into the HTTP responses listed in
|
|
452
|
+
[Errors](https://github.com/alexkorolex/litestar_keycloak_admin#errors) wherever they are raised.
|
|
453
|
+
|
|
454
|
+
## Public API
|
|
455
|
+
|
|
456
|
+
Everything below is importable from `litestar_keycloak_admin`; other modules are internal.
|
|
457
|
+
|
|
458
|
+
| Name | Kind |
|
|
459
|
+
| --- | --- |
|
|
460
|
+
| `KeycloakAdminPlugin` | The plugin to register on the application |
|
|
461
|
+
| `KeycloakAdminConfig`, `RefreshCookieConfig` | Configuration |
|
|
462
|
+
| `KeycloakAdminClient` | Asynchronous client for Keycloak's token endpoints and Admin REST API |
|
|
463
|
+
| `requires_admin` | Guard: the caller holds `admin_role` |
|
|
464
|
+
| `KeycloakSessionController`, `KeycloakAccountController`, `UserResponse` | The built-in endpoints and their user model |
|
|
465
|
+
| `handle_http_exception`, `handle_internal_error`, `error_response`, `ErrorItem` | The error format |
|
|
466
|
+
| `KeycloakClientError`, `KeycloakUnavailableError`, `KeycloakLoginError`, `PasswordChangeRequiredError`, `KeycloakAdminError` | Client exceptions |
|
|
467
|
+
|
|
468
|
+
The package follows [Semantic Versioning](https://semver.org/); while it is in `0.x`, a minor
|
|
469
|
+
release may contain breaking changes, which are listed in the
|
|
470
|
+
[changelog](https://github.com/alexkorolex/litestar_keycloak_admin/blob/main/CHANGELOG.md).
|
|
471
|
+
|
|
472
|
+
## Security notes
|
|
473
|
+
|
|
474
|
+
- **Password grant.** The login endpoints use OAuth's *Resource Owner Password Credentials*
|
|
475
|
+
grant: the password passes through your backend to Keycloak and is never stored. It is what
|
|
476
|
+
allows a custom login form, but OAuth 2.1 deprecates it, and it rules out Keycloak features
|
|
477
|
+
that need its own login page (social login, WebAuthn, its two-factor flows). If you need them,
|
|
478
|
+
use `litestar-keycloak`'s own routes (Authorization Code flow) instead of the public endpoints
|
|
479
|
+
here.
|
|
480
|
+
- **Refresh token.** By default it stays in an `httponly`, `secure`, `SameSite=Strict` cookie
|
|
481
|
+
scoped to `auth_path`, out of reach of JavaScript. `secure` cookies need HTTPS; disable it only
|
|
482
|
+
for local development.
|
|
483
|
+
- **Admin credentials.** Prefer a service account with `manage-users` and `view-realm` to a
|
|
484
|
+
master-realm administrator in production.
|
|
485
|
+
|
|
486
|
+
## Development
|
|
487
|
+
|
|
488
|
+
```bash
|
|
489
|
+
uv sync --group dev
|
|
490
|
+
uv run ruff check .
|
|
491
|
+
uv run ruff format --check .
|
|
492
|
+
uv run pytest
|
|
493
|
+
uv build
|
|
494
|
+
uv run twine check --strict dist/*
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
The tests start a fake Keycloak on `aiohttp.web` (`tests/fake_keycloak.py`), so they run real
|
|
498
|
+
HTTP against both this package and `litestar-keycloak` without a Keycloak instance.
|
|
499
|
+
|
|
500
|
+
See [CONTRIBUTING.md](https://github.com/alexkorolex/litestar_keycloak_admin/blob/main/CONTRIBUTING.md)
|
|
501
|
+
for contribution guidelines,
|
|
502
|
+
[CHANGELOG.md](https://github.com/alexkorolex/litestar_keycloak_admin/blob/main/CHANGELOG.md)
|
|
503
|
+
for user-visible changes, and the
|
|
504
|
+
[release guide](https://github.com/alexkorolex/litestar_keycloak_admin/blob/main/docs/releasing.md)
|
|
505
|
+
for the release process.
|
|
506
|
+
|
|
507
|
+
## License
|
|
508
|
+
|
|
509
|
+
Distributed under the
|
|
510
|
+
[MIT License](https://github.com/alexkorolex/litestar_keycloak_admin/blob/main/LICENSE).
|