feat(backend): JIT-provision the local User on first Authentik login

AuthentikStrategy no longer rejects a valid token whose user has no local
row — it creates the User from the token claims (given_name/family_name/
email) via the new shared resolveOrProvisionAuthentikUser helper, which is
race-safe (P2002 -> re-read) and captures the User to the sync log. The WS
token path (TokenVerificationService.verifyAuthentik) and OnboardingService
now use the same helper, removing three copies of the lookup/create logic.

A provisioned user still has no Membership and therefore no rights: LT role
assignment from Authentik groups is the remaining gap; Verantwortliche go
through the onboarding approval flow.

Tests: provision-user.spec.ts (existing/new/race/rethrow); npm test green
at 51. Docs updated.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-10 08:05:18 +02:00
co-authored by Claude Sonnet 5
parent dbaafabcf4
commit 5079d48905
7 changed files with 206 additions and 60 deletions
+16 -11
View File
@@ -20,12 +20,16 @@ client's host - no separate web server is needed.
## Auth model
- Leitungsteam and Gemeinde Verantwortliche are provisioned in Authentik
(the "Konfi-Castle-ID"); this API acts as an OIDC **resource server**,
verifying access tokens against Authentik's JWKS (`AuthentikStrategy`) and
then resolving local `Membership` rows to determine role + KC/Gemeinde
scope. Clients perform the actual Authorization Code + PKCE flow against
Authentik directly.
- Leitungsteam and Gemeinde Verantwortliche sign in with Authentik (the
"Konfi-Castle-ID"); this API acts as an OIDC **resource server**, verifying
access tokens against Authentik's JWKS (`AuthentikStrategy`). The local
`User` is provisioned just-in-time on first login from the token claims
(`resolveOrProvisionAuthentikUser`); role + KC/Gemeinde scope then come
from local `Membership` rows (only `status = ACTIVE` ones count). A freshly
provisioned user has no membership and thus no rights until one is granted
(LT: manually for now; Verantwortliche: the `onboarding/` approval flow).
Clients perform the Authorization Code + PKCE flow against Authentik
directly.
- Gemeinde Teamer are **local accounts** (no Authentik): a `User` row with a
`passwordHash` and `kcId` set, `authentikSub` left null. A Gemeinde
Verantwortliche/r creates them directly or via a `TeamerInvite`
@@ -105,8 +109,9 @@ client's host - no separate web server is needed.
Leitungsteam roles are global across all KCs).
All planned backend phases are implemented. `npm test` runs Jest unit tests
(`ZuteilungService`, `TeamAuthService`, `TeamerService`, `OnboardingService`;
Prisma mocked). Remaining work: the Flutter clients (see repo root README),
Authentik JIT provisioning for LT (Verantwortliche already self-provision via
`onboarding/`), invite email delivery, and the first real Prisma migration
(only `schema.prisma` exists so far).
(`ZuteilungService`, `TeamAuthService`, `TeamerService`, `OnboardingService`,
`resolveOrProvisionAuthentikUser`; Prisma mocked). Remaining work: the
Flutter clients (see repo root README), deriving the LT `Membership` from
Authentik group claims (the `User` is provisioned, the role is not), invite
email delivery, and the first real Prisma migration (only `schema.prisma`
exists so far).