mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-23 20:56:42 +02:00
🐛 fix(mana-core-auth): use EdDSA for OIDC id_token signing
Set useJWTPlugin: true so id_tokens are signed with EdDSA keys from JWKS instead of HS256. This fixes Synapse OIDC integration which verifies tokens via JWKS endpoint.
This commit is contained in:
parent
5c61a4ed0f
commit
efb077b9ea
22 changed files with 1605 additions and 142 deletions
|
|
@ -13,6 +13,7 @@ import type { TestingModule } from '@nestjs/testing';
|
|||
import { UnauthorizedException } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { JwtAuthGuard } from './jwt-auth.guard';
|
||||
import { LoggerService } from '../logger';
|
||||
import { createMockConfigService, httpMockHelpers } from '../../__tests__/utils/test-helpers';
|
||||
import { mockTokenFactory } from '../../__tests__/utils/mock-factories';
|
||||
import { silentError } from '../../__tests__/utils/silent-error.decorator';
|
||||
|
|
@ -21,6 +22,18 @@ import { jwtVerify } from 'jose';
|
|||
// Mock jose (auto-mocked via jest.config.js moduleNameMapper)
|
||||
jest.mock('jose');
|
||||
|
||||
// Mock LoggerService
|
||||
const createMockLoggerService = (): LoggerService =>
|
||||
({
|
||||
setContext: jest.fn().mockReturnThis(),
|
||||
log: jest.fn(),
|
||||
info: jest.fn(),
|
||||
error: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
debug: jest.fn(),
|
||||
verbose: jest.fn(),
|
||||
}) as unknown as LoggerService;
|
||||
|
||||
describe('JwtAuthGuard', () => {
|
||||
let guard: JwtAuthGuard;
|
||||
let configService: ConfigService;
|
||||
|
|
@ -41,6 +54,10 @@ describe('JwtAuthGuard', () => {
|
|||
'jwt.audience': 'manacore',
|
||||
}),
|
||||
},
|
||||
{
|
||||
provide: LoggerService,
|
||||
useValue: createMockLoggerService(),
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
|
|
@ -344,7 +361,8 @@ describe('JwtAuthGuard', () => {
|
|||
createMockConfigService({
|
||||
'jwt.issuer': 'manacore',
|
||||
'jwt.audience': 'manacore',
|
||||
})
|
||||
}),
|
||||
createMockLoggerService()
|
||||
);
|
||||
|
||||
const mockRequest = httpMockHelpers.createMockRequest({
|
||||
|
|
@ -375,7 +393,8 @@ describe('JwtAuthGuard', () => {
|
|||
BASE_URL: 'http://localhost:3001',
|
||||
'jwt.issuer': 'custom-issuer',
|
||||
'jwt.audience': 'custom-audience',
|
||||
})
|
||||
}),
|
||||
createMockLoggerService()
|
||||
);
|
||||
|
||||
const mockRequest = httpMockHelpers.createMockRequest({
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import {
|
|||
} from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { jwtVerify, createRemoteJWKSet } from 'jose';
|
||||
import { LoggerService } from '../logger';
|
||||
|
||||
/**
|
||||
* JWT Auth Guard using JWKS (Better Auth compatible)
|
||||
|
|
@ -16,17 +17,20 @@ import { jwtVerify, createRemoteJWKSet } from 'jose';
|
|||
@Injectable()
|
||||
export class JwtAuthGuard implements CanActivate {
|
||||
private jwks: ReturnType<typeof createRemoteJWKSet> | null = null;
|
||||
private readonly logger: LoggerService;
|
||||
|
||||
constructor(private configService: ConfigService) {}
|
||||
constructor(
|
||||
private configService: ConfigService,
|
||||
loggerService: LoggerService
|
||||
) {
|
||||
this.logger = loggerService.setContext('JwtAuthGuard');
|
||||
}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const token = this.extractTokenFromHeader(request);
|
||||
|
||||
console.log('[JwtAuthGuard] Token (first 50 chars):', token?.substring(0, 50));
|
||||
|
||||
if (!token) {
|
||||
console.log('[JwtAuthGuard] No token provided');
|
||||
throw new UnauthorizedException('No token provided');
|
||||
}
|
||||
|
||||
|
|
@ -35,21 +39,18 @@ export class JwtAuthGuard implements CanActivate {
|
|||
if (!this.jwks) {
|
||||
const baseUrl = this.configService.get<string>('BASE_URL') || 'http://localhost:3001';
|
||||
const jwksUrl = new URL('/api/v1/auth/jwks', baseUrl);
|
||||
console.log('[JwtAuthGuard] Initializing JWKS from:', jwksUrl.toString());
|
||||
this.jwks = createRemoteJWKSet(jwksUrl);
|
||||
}
|
||||
|
||||
const issuer = this.configService.get<string>('jwt.issuer') || 'manacore';
|
||||
const audience = this.configService.get<string>('jwt.audience') || 'manacore';
|
||||
|
||||
console.log('[JwtAuthGuard] Verifying with issuer:', issuer, 'audience:', audience);
|
||||
|
||||
const { payload } = await jwtVerify(token, this.jwks, {
|
||||
issuer,
|
||||
audience,
|
||||
});
|
||||
|
||||
console.log('[JwtAuthGuard] Verification SUCCESS, user:', payload.sub);
|
||||
this.logger.debug('Token verification successful', { userId: payload.sub });
|
||||
|
||||
// Attach user to request
|
||||
request.user = {
|
||||
|
|
@ -60,7 +61,9 @@ export class JwtAuthGuard implements CanActivate {
|
|||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('[JwtAuthGuard] Token verification FAILED:', error);
|
||||
this.logger.warn('Token verification failed', {
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
});
|
||||
throw new UnauthorizedException('Invalid token');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
2
services/mana-core-auth/src/common/logger/index.ts
Normal file
2
services/mana-core-auth/src/common/logger/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export { LoggerService, getLogger } from './logger.service';
|
||||
export { LoggerModule } from './logger.module';
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import { Global, Module } from '@nestjs/common';
|
||||
import { LoggerService } from './logger.service';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
providers: [LoggerService],
|
||||
exports: [LoggerService],
|
||||
})
|
||||
export class LoggerModule {}
|
||||
95
services/mana-core-auth/src/common/logger/logger.service.ts
Normal file
95
services/mana-core-auth/src/common/logger/logger.service.ts
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
import { Injectable, LoggerService as NestLoggerService, Scope } from '@nestjs/common';
|
||||
import * as winston from 'winston';
|
||||
|
||||
const { combine, timestamp, printf, colorize, json } = winston.format;
|
||||
|
||||
// Custom format for development (readable)
|
||||
const devFormat = printf(({ level, message, timestamp, context, ...meta }) => {
|
||||
const ctx = context ? `[${context}]` : '';
|
||||
const metaStr = Object.keys(meta).length ? ` ${JSON.stringify(meta)}` : '';
|
||||
return `${timestamp} ${level} ${ctx} ${message}${metaStr}`;
|
||||
});
|
||||
|
||||
// Create winston logger instance
|
||||
function createLogger(): winston.Logger {
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
|
||||
return winston.createLogger({
|
||||
level: process.env.LOG_LEVEL || (isProduction ? 'info' : 'debug'),
|
||||
format: isProduction
|
||||
? combine(timestamp(), json())
|
||||
: combine(timestamp({ format: 'HH:mm:ss' }), colorize(), devFormat),
|
||||
transports: [new winston.transports.Console()],
|
||||
// Don't exit on error
|
||||
exitOnError: false,
|
||||
});
|
||||
}
|
||||
|
||||
@Injectable({ scope: Scope.TRANSIENT })
|
||||
export class LoggerService implements NestLoggerService {
|
||||
private logger: winston.Logger;
|
||||
private context?: string;
|
||||
|
||||
constructor() {
|
||||
this.logger = createLogger();
|
||||
}
|
||||
|
||||
setContext(context: string): this {
|
||||
this.context = context;
|
||||
return this;
|
||||
}
|
||||
|
||||
log(message: string, ...optionalParams: unknown[]): void {
|
||||
this.logger.info(message, this.formatMeta(optionalParams));
|
||||
}
|
||||
|
||||
info(message: string, meta?: Record<string, unknown>): void {
|
||||
this.logger.info(message, { context: this.context, ...meta });
|
||||
}
|
||||
|
||||
error(message: string, trace?: string, ...optionalParams: unknown[]): void {
|
||||
this.logger.error(message, {
|
||||
context: this.context,
|
||||
trace,
|
||||
...this.formatMeta(optionalParams),
|
||||
});
|
||||
}
|
||||
|
||||
warn(message: string, ...optionalParams: unknown[]): void {
|
||||
this.logger.warn(message, this.formatMeta(optionalParams));
|
||||
}
|
||||
|
||||
debug(message: string, ...optionalParams: unknown[]): void {
|
||||
this.logger.debug(message, this.formatMeta(optionalParams));
|
||||
}
|
||||
|
||||
verbose(message: string, ...optionalParams: unknown[]): void {
|
||||
this.logger.verbose(message, this.formatMeta(optionalParams));
|
||||
}
|
||||
|
||||
private formatMeta(optionalParams: unknown[]): Record<string, unknown> {
|
||||
const meta: Record<string, unknown> = { context: this.context };
|
||||
|
||||
if (optionalParams.length === 1 && typeof optionalParams[0] === 'string') {
|
||||
// NestJS passes context as last param
|
||||
meta.context = optionalParams[0];
|
||||
} else if (optionalParams.length > 0) {
|
||||
meta.params = optionalParams;
|
||||
}
|
||||
|
||||
return meta;
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton instance for use outside DI (main.ts, scripts)
|
||||
let globalLogger: LoggerService | null = null;
|
||||
|
||||
export function getLogger(context?: string): LoggerService {
|
||||
if (!globalLogger) {
|
||||
globalLogger = new LoggerService();
|
||||
}
|
||||
if (context) {
|
||||
return new LoggerService().setContext(context);
|
||||
}
|
||||
return globalLogger;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue