Authentication & Authorization
Overviewβ
The application implements JWT-based authentication with Azure Entra ID integration for enterprise SSO. This guide covers both token-based authentication for API clients and federated identity for enterprise users.
Authentication Strategiesβ
JWT Authenticationβ
JWT (JSON Web Tokens) provide stateless authentication:
- Token-Based: No session storage on server
- Stateless: Verify signature without database lookup
- Scalable: Works across multiple server instances
- Expirable: Tokens have built-in expiration
Azure Entra IDβ
Azure Entra ID (formerly Azure AD) provides:
- Enterprise SSO: Single sign-on for organization
- Federated Identity: Delegate auth to Microsoft
- Multi-Tenant: Support multiple organizations
- Compliance: Enterprise security standards
JWT Implementationβ
JWT Strategy Setupβ
src/auth/strategies/jwt.strategy.tsβ
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { ConfigService } from '@nestjs/config';
interface JwtPayload {
sub: string;
email: string;
roles: string[];
iat: number;
exp: number;
}
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(configService: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: configService.get('JWT_ACCESS_SECRET'),
});
}
validate(payload: JwtPayload) {
return {
userId: payload.sub,
email: payload.email,
roles: payload.roles,
};
}
}
Auth Serviceβ
src/auth/auth.service.tsβ
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
import * as bcrypt from 'bcrypt';
@Injectable()
export class AuthService {
constructor(
private jwtService: JwtService,
private configService: ConfigService,
private prisma: PrismaService,
) {}
async login(email: string, password: string) {
// Find user
const user = await this.prisma.user.findUnique({
where: { email },
});
if (!user || !user.passwordHash) {
throw new UnauthorizedException('Invalid credentials');
}
// Verify password
const isPasswordValid = await bcrypt.compare(password, user.passwordHash);
if (!isPasswordValid) {
throw new UnauthorizedException('Invalid credentials');
}
// Generate tokens
return this.generateTokens(user.id, user.email, user.roles);
}
async generateTokens(userId: string, email: string, roles: string[]) {
const accessToken = this.jwtService.sign(
{
sub: userId,
email,
roles,
},
{
expiresIn: this.configService.get('JWT_ACCESS_EXPIRES_IN', '15m'),
},
);
const refreshToken = this.jwtService.sign(
{
sub: userId,
type: 'refresh',
},
{
expiresIn: this.configService.get('JWT_REFRESH_EXPIRES_IN', '7d'),
},
);
// Store refresh token in database
await this.prisma.userSession.create({
data: {
userId,
token: refreshToken,
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
},
});
return {
accessToken,
refreshToken,
expiresIn: 900, // 15 minutes in seconds
};
}
async refreshTokens(refreshToken: string) {
// Verify token
const payload = this.jwtService.verify(refreshToken, {
secret: this.configService.get('JWT_REFRESH_SECRET'),
});
// Check if token still valid in database
const session = await this.prisma.userSession.findUnique({
where: { token: refreshToken },
});
if (!session || session.expiresAt < new Date()) {
throw new UnauthorizedException('Refresh token expired');
}
// Get fresh user data
const user = await this.prisma.user.findUnique({
where: { id: payload.sub },
});
if (!user) {
throw new UnauthorizedException('User not found');
}
return this.generateTokens(user.id, user.email, user.roles);
}
async logout(refreshToken: string) {
await this.prisma.userSession.delete({
where: { token: refreshToken },
});
}
}
Auth Controllerβ
src/auth/auth.controller.tsβ
import { Controller, Post, Body, UseGuards, Request, HttpCode, HttpStatus } from '@nestjs/common';
import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
import { ZodValidationPipe } from 'nestjs-zod';
import { AuthService } from './auth.service';
import { LoginSchema } from '@qms/schemas/auth/login.schema';
import { JwtAuthGuard } from './guards/jwt-auth.guard';
import { RefreshTokenGuard } from './guards/refresh-token.guard';
@Controller('auth')
@ApiTags('Authentication')
export class AuthController {
constructor(private authService: AuthService) {}
@Post('login')
@HttpCode(HttpStatus.OK)
async login(
@Body(new ZodValidationPipe(LoginSchema))
{ email, password }: LoginSchema,
) {
const tokens = await this.authService.login(email, password);
return {
data: tokens,
};
}
@Post('refresh')
@HttpCode(HttpStatus.OK)
@UseGuards(RefreshTokenGuard)
async refresh(@Request() req) {
const tokens = await this.authService.refreshTokens(req.user.token);
return {
data: tokens,
};
}
@Post('logout')
@HttpCode(HttpStatus.NO_CONTENT)
@UseGuards(JwtAuthGuard)
async logout(@Request() req) {
await this.authService.logout(req.user.refreshToken);
}
@Post('profile')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
async getProfile(@Request() req) {
return {
data: {
userId: req.user.userId,
email: req.user.email,
roles: req.user.roles,
},
};
}
}
Azure Entra ID Integrationβ
Azure Configurationβ
Environment Variablesβ
AZURE_TENANT_ID=your-tenant-id
AZURE_CLIENT_ID=your-client-id
AZURE_CLIENT_SECRET=your-client-secret
AZURE_REDIRECT_URI=http://localhost:3001/auth/azure/callback
Azure Strategyβ
src/auth/strategies/azure.strategy.tsβ
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { BearerStrategy } from 'passport-azure-ad';
import { ConfigService } from '@nestjs/config';
interface AzureAdProfile {
oid: string;
name: string;
email: string;
upn: string;
}
@Injectable()
export class AzureAdStrategy extends PassportStrategy(BearerStrategy, 'azure-ad') {
constructor(configService: ConfigService) {
super({
identityMetadata: `https://login.microsoftonline.com/${configService.get('AZURE_TENANT_ID')}/v2.0/.well-known/openid-configuration`,
clientID: configService.get('AZURE_CLIENT_ID'),
audience: configService.get('AZURE_CLIENT_ID'),
});
}
validate(payload: AzureAdProfile) {
return {
userId: payload.oid,
email: payload.email,
name: payload.name,
upn: payload.upn,
};
}
}
Azure Authentication Routeβ
src/auth/auth.controller.ts (extended)β
@Post('azure/login')
@UseGuards(AuthGuard('azure-ad'))
@HttpCode(HttpStatus.OK)
async azureLogin(@Request() req) {
// User authenticated by Azure Strategy
const azureUser = req.user;
// Get or create user in database
let user = await this.prisma.user.findUnique({
where: { email: azureUser.email },
});
if (!user) {
user = await this.prisma.user.create({
data: {
email: azureUser.email,
name: azureUser.name,
azureId: azureUser.userId,
authProvider: 'azure',
},
});
}
// Generate JWT tokens
const tokens = await this.authService.generateTokens(
user.id,
user.email,
user.roles,
);
return {
data: tokens,
};
}
Guardsβ
JWT Auth Guardβ
src/auth/guards/jwt-auth.guard.tsβ
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}
Refresh Token Guardβ
src/auth/guards/refresh-token.guard.tsβ
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class RefreshTokenGuard extends AuthGuard('jwt-refresh') {}
Optional Auth Guardβ
@Injectable()
export class OptionalJwtAuthGuard extends AuthGuard('jwt') {
handleRequest(err, user, info, context) {
// Don't throw if no auth - just return user or undefined
return user || null;
}
}
Authorization (RBAC)β
Role-Based Access Controlβ
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from '@nestjs/common';
@Injectable()
export class RolesGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
const requiredRoles = Reflect.getMetadata('roles', context.getHandler());
if (!requiredRoles) {
return true;
}
const userRoles = request.user?.roles || [];
const hasRole = requiredRoles.some((role: string) => userRoles.includes(role));
if (!hasRole) {
throw new ForbiddenException('You do not have permission to access this resource');
}
return true;
}
}
Roles Decoratorβ
import { SetMetadata } from '@nestjs/common';
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
Usage in Controllerβ
@Get('admin-only')
@Roles('admin', 'manager')
@UseGuards(JwtAuthGuard, RolesGuard)
async adminEndpoint() {
return { data: 'Admin data' };
}
Permissions (PBAC)β
Permission-Based Access Controlβ
@Injectable()
export class PermissionsGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
const requiredPermissions = Reflect.getMetadata('permissions', context.getHandler());
if (!requiredPermissions) {
return true;
}
const userPermissions = request.user?.permissions || [];
const hasPermission = requiredPermissions.every((permission: string) =>
userPermissions.includes(permission),
);
if (!hasPermission) {
throw new ForbiddenException('Insufficient permissions');
}
return true;
}
}
export const Permissions = (...permissions: string[]) => SetMetadata('permissions', permissions);
Token Validationβ
Token Expirationβ
interface JwtPayload {
sub: string;
email: string;
iat: number;
exp: number;
}
// JWT automatically validates expiration
// If expired, throws UnauthorizedException
const payload = await jwtService.verifyAsync(token);
Token Revocationβ
// Store revoked tokens
await prisma.revokedToken.create({
data: {
token,
revokedAt: new Date(),
expiresAt: new Date(Date.now() + 15 * 60 * 1000), // 15 minutes
},
});
// Check if revoked before accepting
const revoked = await prisma.revokedToken.findUnique({
where: { token },
});
if (revoked) {
throw new UnauthorizedException('Token has been revoked');
}
Auth Moduleβ
Complete Auth Moduleβ
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { ConfigService } from '@nestjs/config';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { JwtStrategy } from './strategies/jwt.strategy';
import { AzureAdStrategy } from './strategies/azure.strategy';
@Module({
imports: [
PassportModule,
JwtModule.registerAsync({
global: true,
useFactory: async (configService: ConfigService) => ({
secret: configService.get('JWT_ACCESS_SECRET'),
signOptions: { expiresIn: '15m' },
}),
inject: [ConfigService],
}),
],
controllers: [AuthController],
providers: [AuthService, JwtStrategy, AzureAdStrategy],
exports: [AuthService],
})
export class AuthModule {}
Schemasβ
Login Schemaβ
export const LoginSchema = z.object({
email: z.string().email('Invalid email format'),
password: z.string().min(8, 'Password must be at least 8 characters'),
});
export type LoginRequest = z.infer<typeof LoginSchema>;
Environment Configurationβ
# JWT
JWT_ACCESS_SECRET=your-secret-key-min-32-chars
JWT_REFRESH_SECRET=your-refresh-secret-min-32-chars
JWT_ACCESS_EXPIRES_IN=15m
JWT_REFRESH_EXPIRES_IN=7d
# Azure
AZURE_TENANT_ID=your-tenant-id
AZURE_CLIENT_ID=your-client-id
AZURE_CLIENT_SECRET=your-client-secret
Best Practicesβ
1. Use Short-Lived Access Tokensβ
// β
GOOD: 15 minute access token
expiresIn: '15m';
// β AVOID: Long-lived access token
expiresIn: '30d';
2. Implement Token Rotationβ
// Refresh tokens have longer lifetime
// But can be revoked
// Force re-authentication periodically
3. Secure Token Transmissionβ
// β
GOOD: Bearer token in Authorization header
Authorization: Bearer<token>;
// Transmitted over HTTPS only
// Never in URL parameters
4. Validate All Tokensβ
// β
GOOD: Verify and validate all tokens
const payload = await jwtService.verifyAsync(token);
// β AVOID: Assume token is valid
const payload = jwt.decode(token);
Troubleshootingβ
Token Verification Failsβ
# Check secret key matches
echo $JWT_ACCESS_SECRET
# Verify token format
# Bearer <token>
# Check token expiration
Azure Integration Issuesβ
# Verify tenant ID
# Verify client ID
# Check redirect URI matches registered app
# Test with curl
curl -X POST \
-H "Authorization: Bearer <token>" \
http://localhost:3001/auth/profile