feat: Wahl-Phasen, Verantwortliche-Invites, Auth fixes, GRUPPE chat #1
+1
-1
@@ -7,7 +7,7 @@ AUTHENTIK_ISSUER_URL="https://sso.konfi-castle.com/application/o/konfi-castle-ap
|
|||||||
# Name of the Authentik group whose members are Leitungsteam. Mirrored to
|
# Name of the Authentik group whose members are Leitungsteam. Mirrored to
|
||||||
# User.isLeitungsteam on every login (the access token must carry a `groups`
|
# User.isLeitungsteam on every login (the access token must carry a `groups`
|
||||||
# claim; add the "groups" scope to the Authentik provider).
|
# claim; add the "groups" scope to the Authentik provider).
|
||||||
AUTHENTIK_LEITUNGSTEAM_GROUP="Leitungsteam"
|
AUTHENTIK_LEITUNGSTEAM_GROUP="KC-APP-LT"
|
||||||
|
|
||||||
# Secret used to sign guest/Konfi session tokens (local accounts only)
|
# Secret used to sign guest/Konfi session tokens (local accounts only)
|
||||||
GUEST_JWT_SECRET="change-me"
|
GUEST_JWT_SECRET="change-me"
|
||||||
|
|||||||
@@ -7,13 +7,19 @@ import { Request } from 'express';
|
|||||||
import { PrismaClient } from '../prisma/prisma.module';
|
import { PrismaClient } from '../prisma/prisma.module';
|
||||||
import { SyncService } from '../sync/sync.service';
|
import { SyncService } from '../sync/sync.service';
|
||||||
import { AuthenticatedUser } from './authenticated-request';
|
import { AuthenticatedUser } from './authenticated-request';
|
||||||
import { resolveOrProvisionAuthentikUser, toAuthenticatedUser } from './provision-user';
|
import {
|
||||||
|
authentikEmail,
|
||||||
|
resolveOrProvisionAuthentikUser,
|
||||||
|
toAuthenticatedUser,
|
||||||
|
} from './provision-user';
|
||||||
|
|
||||||
interface AuthentikJwtPayload {
|
interface AuthentikJwtPayload {
|
||||||
sub: string;
|
sub: string;
|
||||||
email?: string;
|
email?: string;
|
||||||
given_name?: string;
|
given_name?: string;
|
||||||
family_name?: string;
|
family_name?: string;
|
||||||
|
preferred_username?: string;
|
||||||
|
name?: string;
|
||||||
groups?: string[];
|
groups?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,8 +58,8 @@ export class AuthentikStrategy extends PassportStrategy(Strategy, 'authentik') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async validate(payload: AuthentikJwtPayload): Promise<AuthenticatedUser> {
|
async validate(payload: AuthentikJwtPayload): Promise<AuthenticatedUser> {
|
||||||
if (!payload.email) {
|
if (!payload.sub) {
|
||||||
throw new UnauthorizedException('Authentik token missing email claim');
|
throw new UnauthorizedException('Authentik token missing subject');
|
||||||
}
|
}
|
||||||
const isLeitungsteam = (payload.groups ?? []).includes(this.leitungsteamGroup);
|
const isLeitungsteam = (payload.groups ?? []).includes(this.leitungsteamGroup);
|
||||||
const user = await resolveOrProvisionAuthentikUser(
|
const user = await resolveOrProvisionAuthentikUser(
|
||||||
@@ -61,8 +67,8 @@ export class AuthentikStrategy extends PassportStrategy(Strategy, 'authentik') {
|
|||||||
this.sync,
|
this.sync,
|
||||||
{
|
{
|
||||||
sub: payload.sub,
|
sub: payload.sub,
|
||||||
email: payload.email,
|
email: authentikEmail(payload),
|
||||||
firstName: payload.given_name ?? '',
|
firstName: payload.given_name ?? payload.preferred_username ?? payload.name ?? '',
|
||||||
lastName: payload.family_name ?? '',
|
lastName: payload.family_name ?? '',
|
||||||
},
|
},
|
||||||
isLeitungsteam,
|
isLeitungsteam,
|
||||||
|
|||||||
@@ -10,6 +10,18 @@ export interface AuthentikClaims {
|
|||||||
lastName: string;
|
lastName: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Authentik users don't necessarily have an email set. Fall back to a stable,
|
||||||
|
/// per-user placeholder so provisioning still has a unique handle for the row.
|
||||||
|
export function authentikEmail(p: {
|
||||||
|
email?: string;
|
||||||
|
preferred_username?: string;
|
||||||
|
sub: string;
|
||||||
|
}): string {
|
||||||
|
const e = p.email?.trim();
|
||||||
|
if (e) return e.toLowerCase();
|
||||||
|
return `${p.preferred_username?.trim() || p.sub}@no-email.authentik`.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
/// Placeholder kcId for the synthetic, global LEITUNGSTEAM membership. RolesGuard
|
/// Placeholder kcId for the synthetic, global LEITUNGSTEAM membership. RolesGuard
|
||||||
/// never compares it (LT short-circuits the KC check), it only needs to exist.
|
/// never compares it (LT short-circuits the KC check), it only needs to exist.
|
||||||
export const GLOBAL_LT_KC_ID = '*';
|
export const GLOBAL_LT_KC_ID = '*';
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { GuestJwtPayload } from './guest-auth.service';
|
|||||||
import { TeamAuthService } from './team-auth.service';
|
import { TeamAuthService } from './team-auth.service';
|
||||||
import {
|
import {
|
||||||
AuthentikClaims,
|
AuthentikClaims,
|
||||||
|
authentikEmail,
|
||||||
resolveOrProvisionAuthentikUser,
|
resolveOrProvisionAuthentikUser,
|
||||||
toAuthenticatedUser,
|
toAuthenticatedUser,
|
||||||
} from './provision-user';
|
} from './provision-user';
|
||||||
@@ -55,15 +56,22 @@ export class TokenVerificationService {
|
|||||||
email?: string;
|
email?: string;
|
||||||
given_name?: string;
|
given_name?: string;
|
||||||
family_name?: string;
|
family_name?: string;
|
||||||
|
preferred_username?: string;
|
||||||
|
name?: string;
|
||||||
groups?: string[];
|
groups?: string[];
|
||||||
};
|
};
|
||||||
if (!payload.sub || !payload.email) {
|
const sub = payload.sub;
|
||||||
throw new UnauthorizedException('Authentik token missing subject or email');
|
if (!sub) {
|
||||||
|
throw new UnauthorizedException('Authentik token missing subject');
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
sub: payload.sub,
|
sub,
|
||||||
email: payload.email,
|
email: authentikEmail({
|
||||||
firstName: payload.given_name ?? '',
|
email: payload.email,
|
||||||
|
preferred_username: payload.preferred_username,
|
||||||
|
sub,
|
||||||
|
}),
|
||||||
|
firstName: payload.given_name ?? payload.preferred_username ?? payload.name ?? '',
|
||||||
lastName: payload.family_name ?? '',
|
lastName: payload.family_name ?? '',
|
||||||
isLeitungsteam: (payload.groups ?? []).includes(this.leitungsteamGroup),
|
isLeitungsteam: (payload.groups ?? []).includes(this.leitungsteamGroup),
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user