Authentication is the foundation of any secure application. During my work on the EMA project at Servant, I implemented a comprehensive JWT-based authentication system with role-based access control for four distinct user types: admins, coordinators, advocates, and mothers.
Why JWT?
JSON Web Tokens provide a stateless authentication mechanism that scales well and works seamlessly across different services. Unlike session-based auth, JWTs don't require server-side session storage, making them ideal for modern distributed systems.
The Authentication Flow
Here's how we structured the authentication service in NestJS:
// auth.service.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { UsersService } from '../users/users.service';
import * as bcrypt from 'bcrypt';
@Injectable()
export class AuthService {
constructor(
private usersService: UsersService,
private jwtService: JwtService,
) {}
async validateUser(email: string, password: string): Promise<any> {
const user = await this.usersService.findByEmail(email);
if (!user) {
throw new UnauthorizedException('Invalid credentials');
}
const isPasswordValid = await bcrypt.compare(
password,
user.passwordHash
);
if (!isPasswordValid) {
throw new UnauthorizedException('Invalid credentials');
}
const { passwordHash, ...result } = user;
return result;
}
async login(user: any) {
const payload = {
email: user.email,
sub: user.id,
role: user.role
};
return {
access_token: this.jwtService.sign(payload),
user: {
id: user.id,
email: user.email,
firstName: user.firstName,
lastName: user.lastName,
role: user.role
}
};
}
async refreshToken(userId: string) {
const user = await this.usersService.findById(userId);
if (!user) {
throw new UnauthorizedException('User not found');
}
return this.login(user);
}
}Role-Based Access Control
The real power comes from implementing role-based guards:
// roles.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { UserRole } from '../shared/enums/user-role.enum';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride<UserRole[]>(
'roles',
[context.getHandler(), context.getClass()]
);
if (!requiredRoles) {
return true;
}
const { user } = context.switchToHttp().getRequest();
return requiredRoles.some((role) => user.role === role);
}
}
// roles.decorator.ts
import { SetMetadata } from '@nestjs/common';
import { UserRole } from '../shared/enums/user-role.enum';
export const Roles = (...roles: UserRole[]) => SetMetadata('roles', roles);Now we can protect routes based on user roles:
// referrals.controller.ts
@Controller('referrals')
@UseGuards(JwtAuthGuard, RolesGuard)
export class ReferralsController {
constructor(private readonly referralsService: ReferralsService) {}
@Post()
@Roles(UserRole.COORDINATOR, UserRole.ADVOCATE)
async createReferral(@Body() createReferralDto: CreateReferralDto) {
return this.referralsService.create(createReferralDto);
}
@Get()
@Roles(UserRole.ADMIN, UserRole.COORDINATOR)
async getAllReferrals() {
return this.referralsService.findAll();
}
@Get('my-referrals')
@Roles(UserRole.ADVOCATE)
async getMyReferrals(@Request() req) {
return this.referralsService.findByAdvocate(req.user.id);
}
}JWT Strategy Implementation
The Passport strategy validates tokens on each request:
// jwt.strategy.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(
private configService: ConfigService,
) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: configService.get<string>('JWT_SECRET'),
});
}
async validate(payload: any) {
return {
id: payload.sub,
email: payload.email,
role: payload.role
};
}
}Security Best Practices
Based on 27 years of experience, here are critical security considerations:
- Never store passwords in plain text: Always use bcrypt or similar
const saltRounds = 10;
const hashedPassword = await bcrypt.hash(password, saltRounds);- Use environment variables for secrets:
// .env
JWT_SECRET=your-super-secret-key-change-this
JWT_EXPIRATION=1h-
Implement token refresh mechanisms: Short-lived access tokens with refresh tokens
-
Add rate limiting: Prevent brute force attacks
import { ThrottlerModule } from '@nestjs/throttler';
@Module({
imports: [
ThrottlerModule.forRoot({
ttl: 60,
limit: 10,
}),
],
})
export class AppModule {}- Validate all inputs: Use class-validator
import { IsEmail, IsNotEmpty, MinLength } from 'class-validator';
export class LoginDto {
@IsEmail()
@IsNotEmpty()
email: string;
@IsNotEmpty()
@MinLength(8)
password: string;
}Frontend Integration
On the Next.js side, we used the token for authenticated API requests:
// lib/api-client.ts
export class ApiClient {
private baseUrl: string;
private token: string | null = null;
constructor(baseUrl: string) {
this.baseUrl = baseUrl;
}
setToken(token: string) {
this.token = token;
}
async request(endpoint: string, options: RequestInit = {}) {
const headers = {
'Content-Type': 'application/json',
...(this.token && { Authorization: `Bearer ${this.token}` }),
...options.headers,
};
const response = await fetch(`${this.baseUrl}${endpoint}`, {
...options,
headers,
});
if (!response.ok) {
throw new Error(`API error: ${response.statusText}`);
}
return response.json();
}
}Conclusion
Implementing secure authentication requires attention to detail and following best practices. JWT provides a solid foundation, but security is about the entire system - from password hashing to rate limiting to proper token management.
The investment in a robust authentication system pays dividends in security, scalability, and peace of mind.

Jason Cochran
Sofware Engineer | Cloud Consultant | Founder at Strataga
27 years of experience building enterprise software for oil & gas operators and startups. Specializing in SCADA systems, field data solutions, and AI-powered rapid development. Based in Midland, TX serving the Permian Basin.