ws auth
This commit is contained in:
@@ -3,46 +3,16 @@ import { ConfigModule } from '@nestjs/config';
|
||||
import { PassportModule } from '@nestjs/passport';
|
||||
import { AuthService } from './auth.service';
|
||||
import { JwtStrategy } from './strategies/jwt.strategy';
|
||||
import { JwtVerificationService } from './services/jwt-verification.service';
|
||||
import { UsersModule } from '../users/users.module';
|
||||
|
||||
/**
|
||||
* Authentication Module
|
||||
*
|
||||
* Provides Keycloak OpenID Connect authentication using JWT tokens.
|
||||
* This module configures:
|
||||
* - Passport for authentication strategies
|
||||
* - JWT strategy for validating Keycloak tokens
|
||||
* - Integration with UsersModule for user synchronization
|
||||
*
|
||||
* The module requires the following environment variables:
|
||||
* - JWT_ISSUER: Expected JWT issuer
|
||||
* - JWT_AUDIENCE: Expected JWT audience
|
||||
* - JWKS_URI: URI for fetching Keycloak's public keys
|
||||
*/
|
||||
@Module({
|
||||
imports: [
|
||||
// Import ConfigModule to access environment variables
|
||||
ConfigModule,
|
||||
|
||||
// Import PassportModule for authentication strategies
|
||||
PassportModule.register({ defaultStrategy: 'jwt' }),
|
||||
|
||||
// Import UsersModule to enable user synchronization (with forwardRef to avoid circular dependency)
|
||||
forwardRef(() => UsersModule),
|
||||
],
|
||||
providers: [
|
||||
// Register the JWT strategy for validating Keycloak tokens
|
||||
JwtStrategy,
|
||||
|
||||
// Register the auth service for business logic
|
||||
AuthService,
|
||||
],
|
||||
exports: [
|
||||
// Export AuthService so other modules can use it
|
||||
AuthService,
|
||||
|
||||
// Export PassportModule so guards can be used in other modules
|
||||
PassportModule,
|
||||
],
|
||||
providers: [JwtStrategy, AuthService, JwtVerificationService],
|
||||
exports: [AuthService, PassportModule, JwtVerificationService],
|
||||
})
|
||||
export class AuthModule {}
|
||||
|
||||
79
src/auth/services/jwt-verification.service.spec.ts
Normal file
79
src/auth/services/jwt-verification.service.spec.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { JwtVerificationService } from './jwt-verification.service';
|
||||
|
||||
describe('JwtVerificationService', () => {
|
||||
let service: JwtVerificationService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockConfigService = {
|
||||
get: jest.fn((key: string) => {
|
||||
const config: Record<string, string> = {
|
||||
JWKS_URI: 'https://test.com/.well-known/jwks.json',
|
||||
JWT_ISSUER: 'https://test.com',
|
||||
JWT_AUDIENCE: 'test-audience',
|
||||
};
|
||||
return config[key];
|
||||
}),
|
||||
};
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
JwtVerificationService,
|
||||
{ provide: ConfigService, useValue: mockConfigService },
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<JwtVerificationService>(JwtVerificationService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
describe('extractToken', () => {
|
||||
it('should extract token from auth object', () => {
|
||||
const handshake = {
|
||||
auth: { token: 'test-token' },
|
||||
headers: {},
|
||||
};
|
||||
|
||||
const token = service.extractToken(handshake);
|
||||
|
||||
expect(token).toBe('test-token');
|
||||
});
|
||||
|
||||
it('should extract token from Authorization header', () => {
|
||||
const handshake = {
|
||||
auth: {},
|
||||
headers: { authorization: 'Bearer test-token' },
|
||||
};
|
||||
|
||||
const token = service.extractToken(handshake);
|
||||
|
||||
expect(token).toBe('test-token');
|
||||
});
|
||||
|
||||
it('should prioritize auth.token over header', () => {
|
||||
const handshake = {
|
||||
auth: { token: 'auth-token' },
|
||||
headers: { authorization: 'Bearer header-token' },
|
||||
};
|
||||
|
||||
const token = service.extractToken(handshake);
|
||||
|
||||
expect(token).toBe('auth-token');
|
||||
});
|
||||
|
||||
it('should return undefined when no token present', () => {
|
||||
const handshake = {
|
||||
auth: {},
|
||||
headers: {},
|
||||
};
|
||||
|
||||
const token = service.extractToken(handshake);
|
||||
|
||||
expect(token).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
93
src/auth/services/jwt-verification.service.ts
Normal file
93
src/auth/services/jwt-verification.service.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { verify, type JwtHeader } from 'jsonwebtoken';
|
||||
import { JwksClient, type SigningKey } from 'jwks-rsa';
|
||||
import type { JwtPayload } from '../strategies/jwt.strategy';
|
||||
|
||||
const JWT_ALGORITHM = 'RS256';
|
||||
const BEARER_PREFIX = 'Bearer ';
|
||||
|
||||
@Injectable()
|
||||
export class JwtVerificationService {
|
||||
private readonly logger = new Logger(JwtVerificationService.name);
|
||||
private readonly jwksClient: JwksClient;
|
||||
private readonly issuer: string;
|
||||
private readonly audience: string | undefined;
|
||||
|
||||
constructor(private readonly configService: ConfigService) {
|
||||
const jwksUri = this.configService.get<string>('JWKS_URI');
|
||||
this.issuer = this.configService.get<string>('JWT_ISSUER') || '';
|
||||
this.audience = this.configService.get<string>('JWT_AUDIENCE');
|
||||
|
||||
if (!jwksUri) {
|
||||
throw new Error('JWKS_URI must be configured');
|
||||
}
|
||||
|
||||
if (!this.issuer) {
|
||||
throw new Error('JWT_ISSUER must be configured');
|
||||
}
|
||||
|
||||
this.jwksClient = new JwksClient({
|
||||
jwksUri,
|
||||
cache: true,
|
||||
rateLimit: true,
|
||||
jwksRequestsPerMinute: 5,
|
||||
});
|
||||
|
||||
this.logger.log('JWT Verification Service initialized');
|
||||
}
|
||||
|
||||
async verifyToken(token: string): Promise<JwtPayload> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const getKey = (
|
||||
header: JwtHeader,
|
||||
callback: (err: Error | null, signingKey?: string | Buffer) => void,
|
||||
) => {
|
||||
this.jwksClient.getSigningKey(
|
||||
header.kid,
|
||||
(err: Error | null, key?: SigningKey) => {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
const signingKey = key?.getPublicKey();
|
||||
callback(null, signingKey);
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
verify(
|
||||
token,
|
||||
getKey,
|
||||
{
|
||||
issuer: this.issuer,
|
||||
audience: this.audience,
|
||||
algorithms: [JWT_ALGORITHM],
|
||||
},
|
||||
(err, decoded) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
resolve(decoded as JwtPayload);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
extractToken(handshake: {
|
||||
auth?: { token?: string };
|
||||
headers?: { authorization?: string };
|
||||
}): string | undefined {
|
||||
if (handshake.auth?.token) {
|
||||
return handshake.auth.token;
|
||||
}
|
||||
|
||||
const authHeader = handshake.headers?.authorization;
|
||||
if (authHeader?.startsWith(BEARER_PREFIX)) {
|
||||
return authHeader.replace(BEARER_PREFIX, '');
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user