🔒️ 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:
Wuesteon 2025-12-01 17:16:21 +01:00
parent 1d5f49a6d0
commit 942c588e15
20 changed files with 1126 additions and 314 deletions

View file

@ -30,14 +30,9 @@
"@nestjs/common": "^10.0.0 || ^11.0.0",
"@nestjs/config": "^3.0.0 || ^4.0.0",
"@nestjs/core": "^10.0.0 || ^11.0.0",
"@nestjs/axios": "^3.0.0 || ^4.0.0",
"axios": "^1.6.0",
"jsonwebtoken": "^9.0.0",
"reflect-metadata": "^0.1.13 || ^0.2.0",
"rxjs": "^7.8.0"
"reflect-metadata": "^0.1.13 || ^0.2.0"
},
"devDependencies": {
"@types/jsonwebtoken": "^9.0.0",
"@types/node": "^20.0.0",
"typescript": "^5.0.0"
},

View file

@ -6,20 +6,68 @@ import {
Inject,
Optional,
} from '@nestjs/common';
import * as jwt from 'jsonwebtoken';
import { Reflector } from '@nestjs/core';
import { ConfigService } from '@nestjs/config';
import { MANA_CORE_OPTIONS } from '../mana-core.module';
import { ManaCoreModuleOptions } from '../interfaces/mana-core-options.interface';
import { IS_PUBLIC_KEY } from '../decorators/public.decorator';
interface TokenValidationResponse {
valid: boolean;
payload?: {
sub: string;
email: string;
role: string;
sessionId?: string;
sid?: string;
app_id?: string;
iat?: number;
exp?: number;
};
error?: string;
}
// 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.
*/
@Injectable()
export class AuthGuard implements CanActivate {
constructor(
@Optional()
@Inject(MANA_CORE_OPTIONS)
private readonly options?: ManaCoreModuleOptions
private readonly options?: ManaCoreModuleOptions,
@Optional()
private readonly reflector?: Reflector,
@Optional()
private readonly configService?: ConfigService
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
// Check if route is marked as public
if (this.reflector) {
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(),
context.getClass(),
]);
if (isPublic) {
return true;
}
}
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) {
@ -27,33 +75,19 @@ export class AuthGuard implements CanActivate {
}
try {
// Decode the token to extract user information
// The actual verification happens at the Mana Core middleware level
const decoded = jwt.decode(token) as jwt.JwtPayload | null;
if (!decoded || !decoded.sub) {
throw new UnauthorizedException('Invalid token structure');
}
// Attach user info to request
request.user = {
sub: decoded.sub,
email: decoded.email || '',
role: decoded.role || 'user',
app_id: decoded.app_id,
iat: decoded.iat,
exp: decoded.exp,
};
// Store raw token for downstream services
const userData = await this.validateToken(token);
request.user = userData;
request.accessToken = token;
if (this.options?.debug) {
console.log('[AuthGuard] User authenticated:', decoded.sub);
console.log('[AuthGuard] User authenticated:', userData.sub);
}
return true;
} catch (error) {
if (error instanceof UnauthorizedException) {
throw error;
}
if (this.options?.debug) {
console.error('[AuthGuard] Token validation failed:', error);
}
@ -61,6 +95,75 @@ export class AuthGuard implements CanActivate {
}
}
/**
* Check if auth should be bypassed (development mode)
*/
private shouldBypassAuth(): boolean {
const isDev =
this.configService?.get<string>('NODE_ENV') === 'development' ||
process.env.NODE_ENV === 'development';
const bypassAuth =
this.configService?.get<string>('DEV_BYPASS_AUTH') === 'true' ||
process.env.DEV_BYPASS_AUTH === 'true';
return isDev && bypassAuth;
}
/**
* Get development user data
*/
private getDevUser() {
const devUserId =
this.configService?.get<string>('DEV_USER_ID') ||
process.env.DEV_USER_ID ||
DEFAULT_DEV_USER_ID;
return {
sub: devUserId,
email: 'dev@example.com',
role: 'user',
app_id: this.options?.appId,
};
}
/**
* Validate token with Mana Core Auth service
*/
private async validateToken(token: string): Promise<any> {
const authUrl =
this.configService?.get<string>('MANA_CORE_AUTH_URL') ||
process.env.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');
if (this.options?.debug) {
console.error('[AuthGuard] Token validation failed:', response.status, errorText);
}
throw new UnauthorizedException('Invalid token');
}
const result = (await response.json()) as TokenValidationResponse;
if (!result.valid || !result.payload) {
throw new UnauthorizedException(result.error || 'Invalid token');
}
return {
sub: result.payload.sub,
email: result.payload.email,
role: result.payload.role,
app_id: result.payload.app_id || this.options?.appId,
sessionId: result.payload.sessionId || result.payload.sid,
iat: result.payload.iat,
exp: result.payload.exp,
};
}
private extractTokenFromHeader(request: any): string | undefined {
const authHeader = request.headers.authorization;
if (!authHeader) {

View file

@ -5,19 +5,36 @@ import {
Inject,
Optional,
} from '@nestjs/common';
import * as jwt from 'jsonwebtoken';
import { ConfigService } from '@nestjs/config';
import { MANA_CORE_OPTIONS } from '../mana-core.module';
import { ManaCoreModuleOptions } from '../interfaces/mana-core-options.interface';
interface TokenValidationResponse {
valid: boolean;
payload?: {
sub: string;
email: string;
role: string;
sessionId?: string;
sid?: string;
app_id?: string;
iat?: number;
exp?: number;
};
error?: string;
}
/**
* Optional auth guard - allows unauthenticated requests but still extracts user info if token is present
* Optional auth guard - allows unauthenticated requests but still validates and extracts user info if token is present
*/
@Injectable()
export class OptionalAuthGuard implements CanActivate {
constructor(
@Optional()
@Inject(MANA_CORE_OPTIONS)
private readonly options?: ManaCoreModuleOptions
private readonly options?: ManaCoreModuleOptions,
@Optional()
private readonly configService?: ConfigService
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
@ -31,39 +48,69 @@ export class OptionalAuthGuard implements CanActivate {
}
try {
// Decode the token to extract user information
const decoded = jwt.decode(token) as jwt.JwtPayload | null;
const userData = await this.validateToken(token);
if (decoded && decoded.sub) {
// Attach user info to request
request.user = {
sub: decoded.sub,
email: decoded.email || '',
role: decoded.role || 'user',
app_id: decoded.app_id,
iat: decoded.iat,
exp: decoded.exp,
};
// Store raw token for downstream services
if (userData) {
request.user = userData;
request.accessToken = token;
if (this.options?.debug) {
console.log('[OptionalAuthGuard] User authenticated:', decoded.sub);
console.log('[OptionalAuthGuard] User authenticated:', userData.sub);
}
} else {
request.user = null;
}
} catch (error) {
if (this.options?.debug) {
console.error('[OptionalAuthGuard] Token decode failed:', error);
console.error('[OptionalAuthGuard] Token validation failed:', error);
}
// For optional auth, we allow the request to proceed even if token validation fails
request.user = null;
}
return true;
}
/**
* Validate token with Mana Core Auth service
*/
private async validateToken(token: string): Promise<any | null> {
const authUrl =
this.configService?.get<string>('MANA_CORE_AUTH_URL') ||
process.env.MANA_CORE_AUTH_URL ||
'http://localhost:3001';
try {
const response = await fetch(`${authUrl}/api/v1/auth/validate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token }),
});
if (!response.ok) {
return null;
}
const result = (await response.json()) as TokenValidationResponse;
if (!result.valid || !result.payload) {
return null;
}
return {
sub: result.payload.sub,
email: result.payload.email,
role: result.payload.role,
app_id: result.payload.app_id || this.options?.appId,
sessionId: result.payload.sessionId || result.payload.sid,
iat: result.payload.iat,
exp: result.payload.exp,
};
} catch {
return null;
}
}
private extractTokenFromHeader(request: any): string | undefined {
const authHeader = request.headers.authorization;
if (!authHeader) {

View file

@ -1,7 +1,10 @@
import { ModuleMetadata, Type } from '@nestjs/common';
export interface ManaCoreModuleOptions {
manaServiceUrl: string;
/**
* @deprecated No longer used - auth URL is read from MANA_CORE_AUTH_URL env variable
*/
manaServiceUrl?: string;
appId: string;
serviceKey?: string;
signupRedirectUrl?: string;

View file

@ -1,5 +1,4 @@
import { DynamicModule, Module, Global, Provider } from '@nestjs/common';
import { HttpModule, HttpService } from '@nestjs/axios';
import {
ManaCoreModuleOptions,
ManaCoreModuleAsyncOptions,
@ -16,7 +15,6 @@ export class ManaCoreModule {
static forRoot(options: ManaCoreModuleOptions): DynamicModule {
return {
module: ManaCoreModule,
imports: [HttpModule],
providers: [
{
provide: MANA_CORE_OPTIONS,
@ -34,7 +32,7 @@ export class ManaCoreModule {
return {
module: ManaCoreModule,
imports: [...(options.imports || []), HttpModule],
imports: options.imports || [],
providers: [...asyncProviders, AuthGuard, CreditClientService],
exports: [MANA_CORE_OPTIONS, AuthGuard, CreditClientService],
};

View file

@ -1,6 +1,5 @@
import { Injectable, Inject, Optional, Logger } from '@nestjs/common';
import { HttpService } from '@nestjs/axios';
import { firstValueFrom } from 'rxjs';
import { ConfigService } from '@nestjs/config';
import { MANA_CORE_OPTIONS } from '../mana-core.module';
import { ManaCoreModuleOptions } from '../interfaces/mana-core-options.interface';
@ -26,9 +25,35 @@ export class CreditClientService {
@Inject(MANA_CORE_OPTIONS)
private readonly options?: ManaCoreModuleOptions,
@Optional()
private readonly httpService?: HttpService
private readonly configService?: ConfigService
) {}
private getAuthUrl(): string {
return (
this.configService?.get<string>('MANA_CORE_AUTH_URL') ||
process.env.MANA_CORE_AUTH_URL ||
'http://localhost:3001'
);
}
private getServiceKey(): string {
return (
this.options?.serviceKey ||
this.configService?.get<string>('MANA_CORE_SERVICE_KEY') ||
process.env.MANA_CORE_SERVICE_KEY ||
''
);
}
private getAppId(): string {
return (
this.options?.appId ||
this.configService?.get<string>('APP_ID') ||
process.env.APP_ID ||
''
);
}
async validateCredits(
userId: string,
operation: string,
@ -56,10 +81,11 @@ export class CreditClientService {
}
async getBalance(userId: string): Promise<CreditBalance> {
if (!this.httpService || !this.options?.manaServiceUrl) {
this.logger.warn(
'HTTP service or Mana service URL not configured, returning default balance'
);
const authUrl = this.getAuthUrl();
const serviceKey = this.getServiceKey();
if (!serviceKey) {
this.logger.warn('Service key not configured, returning default balance');
return {
balance: 1000,
freeCreditsRemaining: 100,
@ -69,28 +95,30 @@ export class CreditClientService {
}
try {
const response = await firstValueFrom(
this.httpService.get<CreditBalance>(
`${this.options.manaServiceUrl}/credits/balance/${userId}`,
{
headers: {
'X-Service-Key': this.options.serviceKey || '',
'X-App-Id': this.options.appId || '',
},
}
)
);
const response = await fetch(`${authUrl}/api/v1/credits/balance/${userId}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'X-Service-Key': serviceKey,
'X-App-Id': this.getAppId(),
},
});
return response.data;
if (!response.ok) {
this.logger.warn(`Credit balance request failed: ${response.status}`);
return this.getDefaultBalance();
}
const data = (await response.json()) as CreditBalance;
return {
balance: data.balance || 0,
freeCreditsRemaining: data.freeCreditsRemaining || 0,
totalEarned: data.totalEarned || 0,
totalSpent: data.totalSpent || 0,
};
} catch (error) {
this.logger.error(`Failed to get balance for user ${userId}:`, error);
// Return default balance on error
return {
balance: 1000,
freeCreditsRemaining: 100,
totalEarned: 0,
totalSpent: 0,
};
return this.getDefaultBalance();
}
}
@ -101,37 +129,41 @@ export class CreditClientService {
description: string,
metadata?: Record<string, any>
): Promise<boolean> {
if (!this.httpService || !this.options?.manaServiceUrl) {
this.logger.warn(
'HTTP service or Mana service URL not configured, skipping credit consumption'
);
const authUrl = this.getAuthUrl();
const serviceKey = this.getServiceKey();
if (!serviceKey) {
this.logger.warn('Service key not configured, skipping credit consumption');
return true;
}
try {
await firstValueFrom(
this.httpService.post(
`${this.options.manaServiceUrl}/credits/use`,
{
userId,
amount,
appId: this.options.appId,
description,
metadata: {
operation,
...metadata,
},
const response = await fetch(`${authUrl}/api/v1/credits/use`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Service-Key': serviceKey,
'X-App-Id': this.getAppId(),
},
body: JSON.stringify({
userId,
amount,
appId: this.getAppId(),
description,
metadata: {
operation,
...metadata,
},
{
headers: {
'X-Service-Key': this.options.serviceKey || '',
'X-App-Id': this.options.appId || '',
},
}
)
);
}),
});
if (this.options.debug) {
if (!response.ok) {
const errorText = await response.text().catch(() => 'Unknown error');
this.logger.error(`Failed to consume credits: ${response.status} ${errorText}`);
return false;
}
if (this.options?.debug) {
this.logger.log(`Consumed ${amount} credits for user ${userId}: ${description}`);
}
@ -148,32 +180,38 @@ export class CreditClientService {
description: string,
metadata?: Record<string, any>
): Promise<boolean> {
if (!this.httpService || !this.options?.manaServiceUrl) {
this.logger.warn('HTTP service or Mana service URL not configured, skipping credit refund');
const authUrl = this.getAuthUrl();
const serviceKey = this.getServiceKey();
if (!serviceKey) {
this.logger.warn('Service key not configured, skipping credit refund');
return true;
}
try {
await firstValueFrom(
this.httpService.post(
`${this.options.manaServiceUrl}/credits/refund`,
{
userId,
amount,
appId: this.options.appId,
description,
metadata,
},
{
headers: {
'X-Service-Key': this.options.serviceKey || '',
'X-App-Id': this.options.appId || '',
},
}
)
);
const response = await fetch(`${authUrl}/api/v1/credits/refund`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Service-Key': serviceKey,
'X-App-Id': this.getAppId(),
},
body: JSON.stringify({
userId,
amount,
appId: this.getAppId(),
description,
metadata,
}),
});
if (this.options.debug) {
if (!response.ok) {
const errorText = await response.text().catch(() => 'Unknown error');
this.logger.error(`Failed to refund credits: ${response.status} ${errorText}`);
return false;
}
if (this.options?.debug) {
this.logger.log(`Refunded ${amount} credits for user ${userId}: ${description}`);
}
@ -183,4 +221,13 @@ export class CreditClientService {
return false;
}
}
private getDefaultBalance(): CreditBalance {
return {
balance: 1000,
freeCreditsRemaining: 100,
totalEarned: 0,
totalSpent: 0,
};
}
}

View file

@ -0,0 +1,142 @@
# @manacore/shared-nestjs-auth
Shared authentication utilities for NestJS backends in the Mana Core ecosystem.
## Installation
```bash
pnpm add @manacore/shared-nestjs-auth
```
## Usage
### 1. Configure Environment Variables
```env
# Required: Mana Core Auth service URL
MANA_CORE_AUTH_URL=http://localhost:3001
# Optional: Development mode auth bypass
NODE_ENV=development
DEV_BYPASS_AUTH=true
DEV_USER_ID=your-test-user-id
```
### 2. Use in Controllers
```typescript
import { Controller, Get, UseGuards } from '@nestjs/common';
import { JwtAuthGuard, CurrentUser, CurrentUserData } from '@manacore/shared-nestjs-auth';
@Controller('api')
export class MyController {
// Public endpoint
@Get('health')
health() {
return { status: 'ok' };
}
// Protected endpoint
@Get('profile')
@UseGuards(JwtAuthGuard)
getProfile(@CurrentUser() user: CurrentUserData) {
return {
userId: user.userId,
email: user.email,
role: user.role,
};
}
}
```
### 3. Apply Guard Globally (Optional)
```typescript
// app.module.ts
import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';
import { JwtAuthGuard } from '@manacore/shared-nestjs-auth';
@Module({
providers: [
{
provide: APP_GUARD,
useClass: JwtAuthGuard,
},
],
})
export class AppModule {}
```
## API
### JwtAuthGuard
A NestJS guard that validates JWT tokens via the Mana Core Auth service.
- Extracts Bearer token from `Authorization` header
- Calls `POST /api/v1/auth/validate` on auth service
- Attaches user data to request object
- Supports `DEV_BYPASS_AUTH=true` for development
### CurrentUser Decorator
Parameter decorator to extract the authenticated user from the request.
```typescript
@Get('me')
@UseGuards(JwtAuthGuard)
getMe(@CurrentUser() user: CurrentUserData) {
return user;
}
```
### CurrentUserData Interface
```typescript
interface CurrentUserData {
userId: string; // User ID (from JWT sub claim)
email: string; // User email
role: string; // User role (user, admin, service)
sessionId?: string; // Session ID (optional)
}
```
## How It Works
1. Client sends request with `Authorization: Bearer <token>` header
2. JwtAuthGuard extracts the token
3. Guard calls Mana Core Auth service to validate token
4. On success, user data is attached to `request.user`
5. Controller receives user via `@CurrentUser()` decorator
```
┌─────────────┐ ┌─────────────┐ ┌────────────────┐
│ Client │────>│ Your API │────>│ mana-core-auth │
│ │ │ (NestJS) │ │ (port 3001) │
└─────────────┘ └─────────────┘ └────────────────┘
│ │ │
│ Bearer token │ POST /validate │
│ │ {token} │
│ │<────────────────────│
│ │ {valid, payload} │
<──────────────────│ │
│ Response │ │
```
## Development Mode
Set `DEV_BYPASS_AUTH=true` in development to skip token validation:
```env
NODE_ENV=development
DEV_BYPASS_AUTH=true
DEV_USER_ID=17cb0be7-058a-4964-9e18-1fe7055fd014
```
This will use a mock user for all authenticated requests.
## Related Packages
- `@manacore/shared-auth` - Client-side auth for web/mobile apps
- `@manacore/shared-types` - Shared TypeScript types

View file

@ -0,0 +1,33 @@
{
"name": "@manacore/shared-nestjs-auth",
"version": "1.0.0",
"description": "Shared authentication utilities for NestJS backends - JWT validation via Mana Core Auth",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "tsc",
"clean": "rm -rf dist",
"prepublishOnly": "pnpm build"
},
"files": [
"dist"
],
"peerDependencies": {
"@nestjs/common": "^10.0.0 || ^11.0.0",
"@nestjs/config": "^3.0.0 || ^4.0.0"
},
"devDependencies": {
"@nestjs/common": "^10.0.0",
"@nestjs/config": "^3.0.0",
"@types/node": "^20.0.0",
"typescript": "^5.0.0"
},
"keywords": [
"nestjs",
"auth",
"jwt",
"manacore"
],
"author": "Mana Core Team",
"license": "MIT"
}

View file

@ -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;
}
);

View 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;
}
}

View 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';

View 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;
}

View file

@ -0,0 +1,24 @@
{
"compilerOptions": {
"module": "commonjs",
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "ES2021",
"sourceMap": true,
"outDir": "./dist",
"rootDir": "./src",
"baseUrl": "./",
"incremental": true,
"skipLibCheck": true,
"strictNullChecks": true,
"noImplicitAny": true,
"strictBindCallApply": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}