groups und impersination

This commit is contained in:
Bastian Wagner
2026-07-14 18:09:43 +02:00
parent f45583f3ea
commit 0dfba43aa6
16 changed files with 583 additions and 17 deletions

View File

@@ -12,11 +12,13 @@ export interface OidcProfile {
subject: string;
email: string;
name?: string;
groups: string[];
}
interface OidcDiscovery {
authorization_endpoint: string;
token_endpoint: string;
userinfo_endpoint?: string;
jwks_uri: string;
issuer: string;
}
@@ -29,6 +31,7 @@ interface PendingOidcState {
interface TokenResponse {
id_token?: string;
access_token?: string;
error?: string;
error_description?: string;
}
@@ -59,7 +62,7 @@ export class OidcService {
authorizationUrl.searchParams.set('response_type', 'code');
authorizationUrl.searchParams.set('client_id', config.clientId);
authorizationUrl.searchParams.set('redirect_uri', config.callbackUrl);
authorizationUrl.searchParams.set('scope', 'openid email profile');
authorizationUrl.searchParams.set('scope', config.scope);
authorizationUrl.searchParams.set('state', state);
authorizationUrl.searchParams.set('nonce', nonce);
authorizationUrl.searchParams.set('code_challenge', codeChallenge);
@@ -89,6 +92,7 @@ export class OidcService {
pendingState.codeVerifier,
);
console.log(tokenResponse)
if (!tokenResponse.id_token) {
throw new ServiceUnavailableException(
tokenResponse.error_description ??
@@ -120,10 +124,19 @@ export class OidcService {
throw new BadRequestException('OIDC email claim is missing.');
}
const idTokenGroups = this.extractGroups(payload, config.groupsClaim);
return {
subject: payload.sub,
email,
name: typeof payload.name === 'string' ? payload.name : undefined,
groups: idTokenGroups.length
? idTokenGroups
: await this.requestUserInfoGroups(
discovery.userinfo_endpoint,
tokenResponse.access_token,
config.groupsClaim,
),
};
}
@@ -210,9 +223,53 @@ export class OidcService {
clientId,
callbackUrl,
clientSecret: process.env.OIDC_CLIENT_SECRET,
scope: process.env.OIDC_SCOPE ?? 'openid email profile',
groupsClaim: process.env.OIDC_GROUPS_CLAIM ?? 'groups',
};
}
private async requestUserInfoGroups(
userInfoEndpoint: string | undefined,
accessToken: string | undefined,
groupsClaim: string,
): Promise<string[]> {
if (!userInfoEndpoint || !accessToken) {
return [];
}
const response = await fetch(userInfoEndpoint, {
headers: { Authorization: `Bearer ${accessToken}` },
});
if (!response.ok) {
return [];
}
const payload = (await response.json().catch(() => ({}))) as Record<
string,
unknown
>;
return this.extractGroups(payload, groupsClaim);
}
private extractGroups(
payload: Record<string, unknown>,
groupsClaim: string,
): string[] {
const claimValue = payload[groupsClaim];
if (!Array.isArray(claimValue)) {
return [];
}
return [...new Set(claimValue)]
.filter((group): group is string => typeof group === 'string')
.map((group) => group.trim())
.filter(Boolean)
.sort((left, right) => left.localeCompare(right));
}
private createOpaqueToken(): string {
return randomBytes(32).toString('base64url');
}