@paul999/nestjs-spa-oidc
v1.0.3
Published
A reusable NestJS module for OIDC (OpenID Connect) authentication in Single Page Applications - forked with DPoP support
Maintainers
Readme
NestJS SPA OIDC
A reusable NestJS module for OIDC (OpenID Connect) authentication in Single Page Applications. This module provides seamless integration with OIDC providers using JWT tokens, with built-in support for authentication guards, role-based access control, and platform-agnostic request handling.
Features
- ✅ OIDC/JWT Token Validation - Automatic token verification using
oidc-spa/backend - ✅ Global Authentication Guard - Protect all routes by default with opt-out via
@Public()decorator - ✅ Role-Based Access Control (RBAC) - Easy role checking with
@Roles()decorator - ✅ Platform Agnostic - Works with both Express and Fastify
- ✅ TypeScript First - Full type safety with customizable token schemas
- ✅ Flexible Configuration - Synchronous and asynchronous configuration options
- ✅ Custom Logger Support - Bring your own logger or use the built-in console logger
- ✅ User Context Extraction - Simple
@User()decorator to access authenticated user
Installation
pnpm add @mwolf1989/nestjs-spa-oidc
# or
npm install @mwolf1989/nestjs-spa-oidc
# or
yarn add @mwolf1989/nestjs-spa-oidcQuick Start
1. Basic Configuration
import { Module } from '@nestjs/common';
import { OidcSpaModule } from '@mwolf1989/nestjs-spa-oidc';
@Module({
imports: [
OidcSpaModule.forRoot({
issuerUri: 'https://auth.example.com/realms/myrealm',
audience: 'account',
}),
],
})
export class AppModule {}2. Async Configuration with ConfigService
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { OidcSpaModule } from '@mwolf1989/nestjs-spa-oidc';
@Module({
imports: [
ConfigModule.forRoot(),
OidcSpaModule.forRootAsync({
imports: [ConfigModule],
useFactory: (configService: ConfigService) => ({
issuerUri: configService.get('OIDC_ISSUER_URI'),
audience: configService.get('OIDC_AUDIENCE'),
}),
inject: [ConfigService],
}),
],
})
export class AppModule {}3. Using in Controllers
import { Controller, Get } from '@nestjs/common';
import { Public, User, Roles } from '@mwolf1989/nestjs-spa-oidc';
import type { DefaultDecodedAccessToken } from '@mwolf1989/nestjs-spa-oidc';
@Controller('api')
export class ApiController {
// Public route - no authentication required
@Public()
@Get('health')
getHealth() {
return { status: 'ok' };
}
// Protected route - requires authentication
@Get('profile')
getProfile(@User() user: DefaultDecodedAccessToken) {
return {
userId: user.sub,
roles: user.realm_access?.roles || [],
};
}
// Role-protected route - requires specific role
@Roles('admin')
@Get('admin')
getAdminData(@User() user: DefaultDecodedAccessToken) {
return { message: 'Admin only data' };
}
// Multiple roles - user needs at least one
@Roles('admin', 'moderator')
@Get('moderation')
getModerationData() {
return { message: 'Moderation data' };
}
// Extract specific user property
@Get('user-id')
getUserId(@User('sub') userId: string) {
return { userId };
}
}Advanced Configuration
Custom Token Schema
Define your own token structure with Zod:
import { z } from 'zod';
import { OidcSpaModule } from '@mwolf1989/nestjs-spa-oidc';
const CustomTokenSchema = z.object({
sub: z.string(),
aud: z.union([z.string(), z.array(z.string())]),
email: z.string().email(),
name: z.string(),
roles: z.array(z.string()),
organization: z.string().optional(),
});
type CustomToken = z.infer<typeof CustomTokenSchema>;
@Module({
imports: [
OidcSpaModule.forRoot<CustomToken>({
issuerUri: 'https://auth.example.com',
audience: 'my-api',
decodedAccessTokenSchema: CustomTokenSchema,
getRolesFromDecodedAccessToken: (token) => token.roles, // Custom role extraction
}),
],
})
export class AppModule {}Custom Logger
Integrate with your existing logger (e.g., Pino, Winston):
import { Logger } from 'nestjs-pino';
import { OidcSpaModule, OidcLogger } from '@mwolf1989/nestjs-spa-oidc';
class PinoOidcLogger implements OidcLogger {
constructor(private readonly logger: Logger) {}
log(message: string, context?: string): void {
this.logger.log({ context }, message);
}
error(message: string, trace?: string, context?: string): void {
this.logger.error({ context, trace }, message);
}
warn(message: string, context?: string): void {
this.logger.warn({ context }, message);
}
debug(message: string, context?: string): void {
this.logger.debug({ context }, message);
}
}
@Module({
imports: [
OidcSpaModule.forRootAsync({
imports: [LoggerModule],
useFactory: (logger: Logger) => ({
issuerUri: process.env.OIDC_ISSUER_URI,
audience: process.env.OIDC_AUDIENCE,
logger: new PinoOidcLogger(logger),
}),
inject: [Logger],
}),
],
})
export class AppModule {}Disable Global Guards
If you prefer to apply guards manually:
OidcSpaModule.forRoot({
issuerUri: 'https://auth.example.com',
audience: 'account',
globalGuard: false, // Disable global AuthGuard
globalRolesGuard: false, // Disable global RolesGuard
})Then apply guards manually in controllers:
import { Controller, UseGuards } from '@nestjs/common';
import { AuthGuard, RolesGuard } from '@mwolf1989/nestjs-spa-oidc';
@Controller('api')
@UseGuards(AuthGuard, RolesGuard)
export class ApiController {
// Your routes here
}Configuration Options
| Option | Type | Required | Default | Description |
|--------|------|----------|---------|-------------|
| issuerUri | string | Yes | - | OIDC issuer URI (e.g., https://auth.example.com/realms/myrealm) |
| audience | string | Yes | - | Expected audience for the access token |
| decodedAccessTokenSchema | ZodType | No | DefaultDecodedAccessTokenSchema | Zod schema for token validation |
| globalGuard | boolean | No | true | Apply AuthGuard globally |
| globalRolesGuard | boolean | No | true | Apply RolesGuard globally |
| logger | OidcLogger | No | ConsoleLogger | Custom logger instance |
| getRolesFromDecodedAccessToken | function | No | (token) => token.realm_access?.roles \|\| [] | Function to extract roles from token |
API Reference
Decorators
@Public()- Mark a route as public (no authentication required)@User(property?)- Extract authenticated user from request@Roles(...roles)- Require specific roles for route access
Guards
AuthGuard- Validates JWT tokens and attaches user to requestRolesGuard- Checks if user has required roles
Services
OidcService- Core service for token validation and user info extractiondecodeAccessToken(authHeader, requiredRole?)- Decode and validate tokengetUserId(token)- Extract user ID from tokengetUserRoles(token)- Extract roles from tokenhasRole(token, role)- Check if user has specific role
Environment Variables
OIDC_ISSUER_URI=https://auth.example.com/realms/myrealm
OIDC_AUDIENCE=accountLicense
MIT
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
