Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
d3d98ab
docs: add design spec for pluggable auth + Keycloak module
antosubash May 27, 2026
3b7426d
docs: add implementation plan for pluggable auth + Keycloak module
antosubash May 27, 2026
543e14c
feat(auth): add AuthProvider protocol for swappable auth backends
antosubash May 27, 2026
1b39868
feat(auth): add auth_provider slot to AuthState
antosubash May 27, 2026
ab0fafa
feat(auth): add provider-agnostic AuthMiddleware
antosubash May 27, 2026
3c74944
refactor(auth,users): move AuthMiddleware + principal_serializer to a…
antosubash May 27, 2026
808b78f
feat(users): implement UsersAuthProvider with session-cookie resolution
antosubash May 27, 2026
64ab4a3
refactor(users): delegate to auth.middleware, keep thin re-export for…
antosubash May 27, 2026
1af0c19
feat(diagnostics): SM020/SM021 — exactly one auth provider required
antosubash May 27, 2026
1e974b5
feat(keycloak): scaffold keycloak module with settings + provider reg…
antosubash May 27, 2026
bbbd895
feat(keycloak): JWKS key cache with JWT validation and key-rotation r…
antosubash May 27, 2026
27c1601
feat(keycloak): OIDC client with authorization URL, token exchange, l…
antosubash May 27, 2026
97d0018
feat(keycloak): KeycloakUserCache model
antosubash May 27, 2026
47e1b90
feat(keycloak): KeycloakAuthProvider with JWT resolution and role map…
antosubash May 27, 2026
23b567c
feat(keycloak): OIDC login/callback endpoints + Inertia login/logout …
antosubash May 27, 2026
f83381a
feat(users): bearer token + refresh token endpoints for mobile/API auth
antosubash May 27, 2026
9d42e58
test: integration tests for pluggable auth provider system
antosubash May 27, 2026
67f89b3
docs: document pluggable auth provider contract and SM020/SM021 diagn…
antosubash May 27, 2026
d455086
fix(keycloak): require issuer + audience for JWT validation
antosubash May 27, 2026
02afc2a
fix(users): resolve bearer tokens in UsersAuthProvider
antosubash May 27, 2026
cdf6a0b
fix(users): API logout clears session + constant-time token login
antosubash May 27, 2026
3bea0b3
fix(keycloak): await inertia.render() in login view
antosubash May 27, 2026
2416cf2
chore: regenerate i18n keys for keycloak module pages
antosubash May 27, 2026
854b2e4
style: auto-format new files with ruff
antosubash May 27, 2026
d3149c9
style: fix ruff lint errors (B904, RUF059, UP017)
antosubash May 27, 2026
34ef198
style: format keycloak TSX pages with biome
antosubash May 27, 2026
94fbfba
style: use module dependency constants in KeycloakModule
antosubash May 27, 2026
ba8759f
style: use page constant for Inertia render
antosubash May 27, 2026
d8c6182
chore(keycloak): add README.md and fix pyproject metadata
antosubash May 27, 2026
238b7dc
Merge branch 'main' into worktree-pluggable-auth-keycloak-design
antosubash May 27, 2026
098aae1
fix(ci): update lockfile, fix httpx_mock strict mode, ignore ty Confi…
antosubash May 27, 2026
7a44c6d
fix(ci): regenerate i18n keys with all modules, fix JWKS test mock
antosubash May 27, 2026
8639029
fix(ci): remove unused imports, exclude keycloak from E2E smoke job
antosubash May 27, 2026
642c6e1
fix(ci): rewrite JWKS refetch test to monkey-patch _fetch_keys directly
antosubash May 27, 2026
806996e
chore: add QA verification screenshots
antosubash May 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/pr.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -147,6 +147,9 @@ jobs:
SM_SECRET_KEY: ci-test-key
SM_USERS_BOOTSTRAP_EMAIL: admin@example.com
SM_USERS_BOOTSTRAP_PASSWORD: admin
# Exclude Keycloak module — SM020 prevents both users and keycloak
# from running simultaneously. E2E tests use the users module.
SM_MODULES_ENABLED: '["Auth","Users","Dashboard","Permissions","Settings","BackgroundTasks","FileStorage","FeatureFlags"]'
E2E_BASE_URL: http://localhost:8000
steps:
- uses: actions/checkout@v6
Expand Down
Binary file added.verify/01-login-page.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added.verify/02-dashboard.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added.verify/03-keycloak-login.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added.verify/04-keycloak-authenticated.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added.verify/05-session-survives-expiry.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added.verify/06-after-logout.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -78,7 +78,7 @@ Standard mixins in `simple_module_db.mixins`: `AuditMixin`, `SoftDeleteMixin` (b

## Diagnostic codes

Meaningful codes when reading `make doctor` output: `SM001` missing meta (error), `SM003` orphan page / `SM004` phantom render (warn), `SM007` module overrides no hooks (info), `SM008` duplicate name (error), `SM009` framework→plugin import (error), `SM010` DB revision behind head (error), `SM011` module table not in migration history (warn), `SM012` `register_settings` overridden but nothing on `app.state.<module>` (warn, fires at dev boot only), `SM013`–`SM016` locale issues, `SM017` module ships `.tsx` pages but is missing `package.json`/`tsconfig.json` (warn), `SM018` Inertia `router.{post,patch,put,delete}()` in a page targets a JSON `/api/*` endpoint (warn — Inertia rejects non-Inertia responses), `SM019` module registers view routes (non-empty `view_prefix` + overrides `register_routes`) but overrides neither `register_menu_items` nor `register_permissions` (warn — pages exist with no sidebar entry and no role-editor visibility; admins can't reach them through the UI). Modules whose views are sub-pages of another module typically register permissions to stay discoverable in the role editor without needing their own sidebar entry. In production, errors fail boot.
Meaningful codes when reading `make doctor` output: `SM001` missing meta (error), `SM003` orphan page / `SM004` phantom render (warn), `SM007` module overrides no hooks (info), `SM008` duplicate name (error), `SM009` framework→plugin import (error), `SM010` DB revision behind head (error), `SM011` module table not in migration history (warn), `SM012` `register_settings` overridden but nothing on `app.state.<module>` (warn, fires at dev boot only), `SM013`–`SM016` locale issues, `SM017` module ships `.tsx` pages but is missing `package.json`/`tsconfig.json` (warn), `SM018` Inertia `router.{post,patch,put,delete}()` in a page targets a JSON `/api/*` endpoint (warn — Inertia rejects non-Inertia responses), `SM019` module registers view routes (non-empty `view_prefix` + overrides `register_routes`) but overrides neither `register_menu_items` nor `register_permissions` (warn — pages exist with no sidebar entry and no role-editor visibility; admins can't reach them through the UI). Modules whose views are sub-pages of another module typically register permissions to stay discoverable in the role editor without needing their own sidebar entry. `SM020` multiple auth provider modules installed (error), `SM021` no auth provider module installed (warn). In production, errors fail boot.

## Tests & fixtures

Expand Down
23 changes: 22 additions & 1 deletion docs/framework-conventions.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -241,12 +241,31 @@ async def create_order(...): ...

The `auth` module exposes a principal-resolver chain on
`app.state.auth.principal_resolvers` — a list of async callables that
`users.AuthMiddleware` consults after the session-cookie path. Use it to add
`AuthMiddleware` consults after the session-cookie path. Use it to add
non-cookie credential sources (Personal Access Tokens, API keys, JWTs)
without forking the middleware. See
[`docs/framework/principal-resolvers.md`](framework/principal-resolvers.md)
for the contract, ordering rules, and a worked Bearer-token example.

### Auth Provider Contract

The framework supports swappable authentication backends. Exactly one auth provider
module must be installed — either `simple-module-users` (local credentials + OAuth)
or `simple-module-keycloak` (Keycloak OIDC). Both implement the `AuthProvider`
protocol from `auth.contracts.provider`.

**Module authors never import from `users` or `keycloak` directly.** Use only:
- `from auth.deps import CurrentUser, require_permission`
- `from auth.contracts.schemas import UserContext`

The `AuthMiddleware` (in `auth/middleware.py`) delegates to the active provider's
`resolve_user()` method, then falls through to the principal-resolver chain.
API paths (`/api/*`) receive 401 JSON when unauthenticated; view paths receive
a 302 redirect to the provider's login URL.

Boot-time diagnostic `SM020` fails if multiple auth providers are installed.
`SM021` warns if none is installed.

## Events

Base class: `Event` from `simple_module_core.events`. Subclass per domain event:
Expand DownExpand Up@@ -290,6 +309,8 @@ Dispatch walks the event's MRO, so subscribing to a base class delivers subclass
| SM014 | WARNING | Non-default locale missing keys present in the default |
| SM015 | WARNING | Non-default locale has keys not in the default |
| SM016 | ERROR | Locale JSON invalid or contains non-string leaves |
| SM020 | ERROR | Multiple auth provider modules installed |
| SM021 | WARNING | No auth provider module installed |

Diagnostics run automatically at boot — warnings print to stderr in dev, errors abort startup in production. There's no separate "run diagnostics" step in a scaffolded app: just start the server.

Expand Down
Loading
Loading