fix(security): Befunde aus dem Security-Audit beheben #7
No files matched your search
@@ -23,6 +23,8 @@ services:
|
|||||||
- OLLAMA_MODEL=${OLLAMA_MODEL:-llava}
|
- OLLAMA_MODEL=${OLLAMA_MODEL:-llava}
|
||||||
- SCANNER_ARCHIVE_DIR=${SCANNER_ARCHIVE_DIR:-/data/scanner/_processed_archive}
|
- SCANNER_ARCHIVE_DIR=${SCANNER_ARCHIVE_DIR:-/data/scanner/_processed_archive}
|
||||||
- OIDC_ISSUER=${OIDC_ISSUER:-}
|
- OIDC_ISSUER=${OIDC_ISSUER:-}
|
||||||
|
# Audience-Prüfung der Tokens – muss dieselbe Client-ID wie im Frontend sein
|
||||||
|
- OIDC_CLIENT_ID=${OIDC_CLIENT_ID:-${VITE_OIDC_CLIENT_ID:-ogOeIlrcsf2spjuwyOc7CuAPrRkRSt3RXfYguoLn}}
|
||||||
- SMTP_HOST=${SMTP_HOST:-}
|
- SMTP_HOST=${SMTP_HOST:-}
|
||||||
- SMTP_PORT=${SMTP_PORT:-587}
|
- SMTP_PORT=${SMTP_PORT:-587}
|
||||||
- SMTP_SECURE=${SMTP_SECURE:-false}
|
- SMTP_SECURE=${SMTP_SECURE:-false}
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { generateKeyPairSync } from 'crypto';
|
||||||
|
import * as jwt from 'jsonwebtoken';
|
||||||
|
import { jwtPruefOptionen } from './jwt-optionen';
|
||||||
|
|
||||||
|
describe('jwtPruefOptionen', () => {
|
||||||
|
it('verlangt die eigene Client-ID als Audience, wenn sie gesetzt ist', () => {
|
||||||
|
expect(
|
||||||
|
jwtPruefOptionen('https://auth.example/application/o/pm/', 'client-pm'),
|
||||||
|
).toEqual({
|
||||||
|
issuer: 'https://auth.example/application/o/pm/',
|
||||||
|
audience: 'client-pm',
|
||||||
|
algorithms: ['RS256'],
|
||||||
|
jwksUri: 'https://auth.example/application/o/pm/jwks/',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('lässt die Audience weg, wenn keine Client-ID konfiguriert ist', () => {
|
||||||
|
const o = jwtPruefOptionen('https://auth.example/application/o/pm', '');
|
||||||
|
expect(o.audience).toBeUndefined();
|
||||||
|
expect(o.jwksUri).toBe('https://auth.example/application/o/pm/jwks/');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Prüft, dass die Optionen in jsonwebtoken (das passport-jwt intern nutzt)
|
||||||
|
// tatsächlich fremde Tokens desselben Issuers abweisen.
|
||||||
|
it('weist Tokens anderer Anwendungen desselben Authentik ab', () => {
|
||||||
|
const { privateKey, publicKey } = generateKeyPairSync('rsa', {
|
||||||
|
modulusLength: 2048,
|
||||||
|
});
|
||||||
|
const o = jwtPruefOptionen('https://auth.example/', 'client-pm');
|
||||||
|
const token = (aud: string) =>
|
||||||
|
jwt.sign({ sub: 'u1' }, privateKey, {
|
||||||
|
algorithm: 'RS256',
|
||||||
|
issuer: o.issuer,
|
||||||
|
audience: aud,
|
||||||
|
});
|
||||||
|
const pruefe = (t: string) =>
|
||||||
|
jwt.verify(t, publicKey, {
|
||||||
|
issuer: o.issuer,
|
||||||
|
audience: o.audience,
|
||||||
|
algorithms: o.algorithms,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(() => pruefe(token('client-pm'))).not.toThrow();
|
||||||
|
expect(() => pruefe(token('client-andere-app'))).toThrow(/audience/);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import type { Algorithm } from 'jsonwebtoken';
|
||||||
|
|
||||||
|
export interface JwtPruefOptionen {
|
||||||
|
issuer: string;
|
||||||
|
/** Client-ID dieser Anwendung; ohne sie wird `aud` nicht geprüft. */
|
||||||
|
audience?: string;
|
||||||
|
algorithms: Algorithm[];
|
||||||
|
jwksUri: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prüfoptionen für Access-Tokens von Authentik. Ohne Audience-Prüfung würde
|
||||||
|
* jedes Token desselben Issuers akzeptiert – also auch eines, das Authentik
|
||||||
|
* für eine andere Anwendung mit demselben Provider ausgestellt hat.
|
||||||
|
*/
|
||||||
|
export function jwtPruefOptionen(
|
||||||
|
issuer: string,
|
||||||
|
clientId: string | undefined,
|
||||||
|
): JwtPruefOptionen {
|
||||||
|
const basis = issuer.endsWith('/') ? issuer.slice(0, -1) : issuer;
|
||||||
|
return {
|
||||||
|
issuer,
|
||||||
|
...(clientId ? { audience: clientId } : {}),
|
||||||
|
algorithms: ['RS256'],
|
||||||
|
jwksUri: `${basis}/jwks/`,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import { PassportStrategy } from '@nestjs/passport';
|
import { PassportStrategy } from '@nestjs/passport';
|
||||||
import { Strategy, ExtractJwt } from 'passport-jwt';
|
import { Strategy, ExtractJwt } from 'passport-jwt';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { passportJwtSecret } from 'jwks-rsa';
|
import { passportJwtSecret } from 'jwks-rsa';
|
||||||
import { mapGroupsToPermissions } from './permissions.enum';
|
import { mapGroupsToPermissions } from './permissions.enum';
|
||||||
|
import { jwtPruefOptionen } from './jwt-optionen';
|
||||||
import { UserIdentitySyncService } from '../user-settings/user-identity-sync.service';
|
import { UserIdentitySyncService } from '../user-settings/user-identity-sync.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -12,20 +13,31 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
|
|||||||
configService: ConfigService,
|
configService: ConfigService,
|
||||||
private readonly identitySync: UserIdentitySyncService,
|
private readonly identitySync: UserIdentitySyncService,
|
||||||
) {
|
) {
|
||||||
const issuer = configService.get<string>('OIDC_ISSUER', '');
|
const optionen = jwtPruefOptionen(
|
||||||
|
configService.get<string>('OIDC_ISSUER', ''),
|
||||||
|
configService.get<string>('OIDC_CLIENT_ID', ''),
|
||||||
|
);
|
||||||
|
|
||||||
super({
|
super({
|
||||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||||
ignoreExpiration: false,
|
ignoreExpiration: false,
|
||||||
issuer,
|
issuer: optionen.issuer,
|
||||||
algorithms: ['RS256'],
|
audience: optionen.audience,
|
||||||
|
algorithms: optionen.algorithms,
|
||||||
secretOrKeyProvider: passportJwtSecret({
|
secretOrKeyProvider: passportJwtSecret({
|
||||||
cache: true,
|
cache: true,
|
||||||
rateLimit: true,
|
rateLimit: true,
|
||||||
jwksRequestsPerMinute: 5,
|
jwksRequestsPerMinute: 5,
|
||||||
jwksUri: `${issuer.endsWith('/') ? issuer.slice(0, -1) : issuer}/jwks/`,
|
jwksUri: optionen.jwksUri,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!optionen.audience) {
|
||||||
|
new Logger(JwtStrategy.name).warn(
|
||||||
|
'OIDC_CLIENT_ID ist nicht gesetzt – die Audience von Tokens wird NICHT geprüft. ' +
|
||||||
|
'Tokens anderer Anwendungen desselben Authentik würden akzeptiert.',
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
validate(payload: any): {
|
validate(payload: any): {
|
||||||
|
|||||||
Reference in new issue
Block a user