mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-25 01:24:38 +02:00
🔒️ feat(auth): centralize JWT validation via mana-core-auth
- Create @manacore/shared-nestjs-auth package with JwtAuthGuard - Update @mana-core/nestjs-integration to validate tokens via auth service - Replace insecure local JWT decode with server-side validation - Integrate Zitare, Presi, ManaDeck backends with centralized auth - Add DEV_BYPASS_AUTH support for development mode - Document auth architecture in CLAUDE.md
This commit is contained in:
parent
1d5f49a6d0
commit
942c588e15
20 changed files with 1126 additions and 314 deletions
|
|
@ -0,0 +1,21 @@
|
|||
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
|
||||
import { CurrentUserData } from '../types';
|
||||
|
||||
/**
|
||||
* Parameter decorator to extract the current user from the request.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* @Get('profile')
|
||||
* @UseGuards(JwtAuthGuard)
|
||||
* getProfile(@CurrentUser() user: CurrentUserData) {
|
||||
* return { userId: user.userId };
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export const CurrentUser = createParamDecorator(
|
||||
(data: unknown, ctx: ExecutionContext): CurrentUserData => {
|
||||
const request = ctx.switchToHttp().getRequest();
|
||||
return request.user;
|
||||
}
|
||||
);
|
||||
133
packages/shared-nestjs-auth/src/guards/jwt-auth.guard.ts
Normal file
133
packages/shared-nestjs-auth/src/guards/jwt-auth.guard.ts
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { TokenValidationResponse, CurrentUserData } from '../types';
|
||||
|
||||
// Default development test user ID
|
||||
const DEFAULT_DEV_USER_ID = '00000000-0000-0000-0000-000000000000';
|
||||
|
||||
/**
|
||||
* JWT Authentication Guard for NestJS backends.
|
||||
*
|
||||
* Validates JWT tokens by calling the Mana Core Auth service.
|
||||
* Supports development mode bypass via DEV_BYPASS_AUTH=true.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // In your controller
|
||||
* @Controller('api')
|
||||
* @UseGuards(JwtAuthGuard)
|
||||
* export class MyController {
|
||||
* @Get('protected')
|
||||
* getProtected(@CurrentUser() user: CurrentUserData) {
|
||||
* return { userId: user.userId };
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Environment variables
|
||||
* MANA_CORE_AUTH_URL=http://localhost:3001
|
||||
* DEV_BYPASS_AUTH=true // Optional: for development
|
||||
* DEV_USER_ID=your-test-user-id // Optional: custom dev user
|
||||
* ```
|
||||
*/
|
||||
@Injectable()
|
||||
export class JwtAuthGuard implements CanActivate {
|
||||
constructor(private configService: ConfigService) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
|
||||
// Development mode: bypass auth if DEV_BYPASS_AUTH is set
|
||||
if (this.shouldBypassAuth()) {
|
||||
request.user = this.getDevUser();
|
||||
return true;
|
||||
}
|
||||
|
||||
const token = this.extractTokenFromHeader(request);
|
||||
|
||||
if (!token) {
|
||||
throw new UnauthorizedException('No token provided');
|
||||
}
|
||||
|
||||
try {
|
||||
const userData = await this.validateToken(token);
|
||||
request.user = userData;
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (error instanceof UnauthorizedException) {
|
||||
throw error;
|
||||
}
|
||||
console.error('[JwtAuthGuard] Error validating token:', error);
|
||||
throw new UnauthorizedException('Token validation failed');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if auth should be bypassed (development mode)
|
||||
*/
|
||||
private shouldBypassAuth(): boolean {
|
||||
const isDev = this.configService.get<string>('NODE_ENV') === 'development';
|
||||
const bypassAuth = this.configService.get<string>('DEV_BYPASS_AUTH') === 'true';
|
||||
return isDev && bypassAuth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get development user data
|
||||
*/
|
||||
private getDevUser(): CurrentUserData {
|
||||
return {
|
||||
userId: this.configService.get<string>('DEV_USER_ID') || DEFAULT_DEV_USER_ID,
|
||||
email: 'dev@example.com',
|
||||
role: 'user',
|
||||
sessionId: 'dev-session',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate token with Mana Core Auth service
|
||||
*/
|
||||
private async validateToken(token: string): Promise<CurrentUserData> {
|
||||
const authUrl =
|
||||
this.configService.get<string>('MANA_CORE_AUTH_URL') || 'http://localhost:3001';
|
||||
|
||||
const response = await fetch(`${authUrl}/api/v1/auth/validate`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ token }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text().catch(() => 'Unknown error');
|
||||
console.error('[JwtAuthGuard] Token validation failed:', response.status, errorText);
|
||||
throw new UnauthorizedException('Invalid token');
|
||||
}
|
||||
|
||||
const result: TokenValidationResponse = await response.json();
|
||||
|
||||
if (!result.valid || !result.payload) {
|
||||
throw new UnauthorizedException(result.error || 'Invalid token');
|
||||
}
|
||||
|
||||
return {
|
||||
userId: result.payload.sub,
|
||||
email: result.payload.email,
|
||||
role: result.payload.role,
|
||||
sessionId: result.payload.sessionId || result.payload.sid,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract Bearer token from Authorization header
|
||||
*/
|
||||
private extractTokenFromHeader(request: any): string | undefined {
|
||||
const authHeader = request.headers.authorization;
|
||||
if (!authHeader) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const [type, token] = authHeader.split(' ');
|
||||
return type === 'Bearer' ? token : undefined;
|
||||
}
|
||||
}
|
||||
29
packages/shared-nestjs-auth/src/index.ts
Normal file
29
packages/shared-nestjs-auth/src/index.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
/**
|
||||
* @manacore/shared-nestjs-auth
|
||||
*
|
||||
* Shared authentication utilities for NestJS backends.
|
||||
* Validates JWT tokens via the central Mana Core Auth service.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { JwtAuthGuard, CurrentUser, CurrentUserData } from '@manacore/shared-nestjs-auth';
|
||||
*
|
||||
* @Controller('api')
|
||||
* @UseGuards(JwtAuthGuard)
|
||||
* export class MyController {
|
||||
* @Get('profile')
|
||||
* getProfile(@CurrentUser() user: CurrentUserData) {
|
||||
* return { userId: user.userId, email: user.email };
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
|
||||
// Guards
|
||||
export { JwtAuthGuard } from './guards/jwt-auth.guard';
|
||||
|
||||
// Decorators
|
||||
export { CurrentUser } from './decorators/current-user.decorator';
|
||||
|
||||
// Types
|
||||
export type { CurrentUserData, AuthModuleConfig, TokenValidationResponse } from './types';
|
||||
40
packages/shared-nestjs-auth/src/types/index.ts
Normal file
40
packages/shared-nestjs-auth/src/types/index.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
/**
|
||||
* User data extracted from JWT token
|
||||
*/
|
||||
export interface CurrentUserData {
|
||||
userId: string;
|
||||
email: string;
|
||||
role: string;
|
||||
sessionId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for the auth module
|
||||
*/
|
||||
export interface AuthModuleConfig {
|
||||
/** URL of the Mana Core Auth service (default: http://localhost:3001) */
|
||||
authServiceUrl?: string;
|
||||
/** Whether to bypass auth in development mode (default: false) */
|
||||
devBypassAuth?: boolean;
|
||||
/** Test user ID for development mode */
|
||||
devUserId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Response from token validation endpoint
|
||||
*/
|
||||
export interface TokenValidationResponse {
|
||||
valid: boolean;
|
||||
payload?: {
|
||||
sub: string;
|
||||
email: string;
|
||||
role: string;
|
||||
sessionId?: string;
|
||||
sid?: string;
|
||||
iat?: number;
|
||||
exp?: number;
|
||||
iss?: string;
|
||||
aud?: string;
|
||||
};
|
||||
error?: string;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue