feat: Authentik OIDC login (PKCE) + Leitungsteam admin screens
Backend: - AuthentikStrategy / TokenVerificationService: normalise the issuer's trailing slash and accept both `iss` spellings (Authentik's discovery issuer and token `iss` carry a trailing slash; the JWKS URL must not double it). Wire the real konfi-castle issuer into .env.example. - team token path now goes through toAuthenticatedUser too, so a local account flagged isLeitungsteam gets the synthetic global LT membership regardless of token kind. - LT-admin controllers (kc, gemeinde, onboarding, sync, teamer) accept ['authentik','team'] so such an account can use them. RolesGuard still enforces the actual LT/role check. - app.module serves the Flutter web build from client/app/build/web (SPA fallback covers the OIDC redirect path /v1/auth/callback), falling back to the interim client/web/ if it isn't built. Client (client/app/): - oidc.dart: Authorization-Code + PKCE against Authentik (discovery, S256 challenge, state, token exchange, refresh). Browser bits (sessionStorage, redirect, URL) behind a conditional import so `flutter test` still compiles on the VM. - AppState handles the ?code= callback on bootstrap, stores access + refresh, refreshes an expired token on restart. - Login screen: "Mit Konfi-Castle-ID anmelden" button (Leitungsteam / Verantwortliche) alongside the local Teamer password form. - admin_screen.dart: LT-only "Verwaltung" — list/create KCs, per KC the Gemeinden (list/create) and pending Verantwortlichen requests (approve/reject). Verified end to end against local Postgres with an isLeitungsteam account (create KC/Gemeinde, list + approve a request). flutter analyze/test/build web green; backend npm test 56. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -29,7 +29,8 @@ export class TokenVerificationService {
|
||||
private readonly teamAuth: TeamAuthService,
|
||||
private readonly sync: SyncService,
|
||||
) {
|
||||
this.issuerUrl = config.getOrThrow<string>('AUTHENTIK_ISSUER_URL');
|
||||
// See AuthentikStrategy: normalise the trailing slash, accept both forms.
|
||||
this.issuerUrl = config.getOrThrow<string>('AUTHENTIK_ISSUER_URL').replace(/\/+$/, '');
|
||||
this.jwks = jwksRsa({ jwksUri: `${this.issuerUrl}/jwks/`, cache: true, rateLimit: true });
|
||||
this.leitungsteamGroup = config.get<string>('AUTHENTIK_LEITUNGSTEAM_GROUP', 'Leitungsteam');
|
||||
}
|
||||
@@ -48,7 +49,7 @@ export class TokenVerificationService {
|
||||
}
|
||||
const key = await this.jwks.getSigningKey(kid);
|
||||
const payload = jwt.verify(token, key.getPublicKey(), {
|
||||
issuer: this.issuerUrl,
|
||||
issuer: [this.issuerUrl, `${this.issuerUrl}/`],
|
||||
algorithms: ['RS256'],
|
||||
}) as jwt.JwtPayload & {
|
||||
email?: string;
|
||||
|
||||
Reference in New Issue
Block a user