Authentication JWT & Roles
Understand JWT authentication, the role system, and how to use decorators and guards.
Overview
The system uses JWT (JSON Web Tokens) for authentication and a roles system for authorization. All routes are protected by default except those marked with the @Public() decorator.
Authentication (JWT)
- • Access Token
- • Refresh Token
- • TTL jitter (spread expirations)
- • Automatic validation via Guards
- • Passport JWT strategy
Authorization (Roles)
- • Flexible role system
- • Decorators for access control
- • Guards for permission validation
- • Multiple roles per user
JWT Authentication Flow
1. User Login
The user sends credentials (email and password) to the login endpoint
POST /api/v1/auth/login
Content-Type: application/json
{
"email": "[email protected]",
"password": "senha123"
}
2. Credential Validation
AuthService validates email and password using Argon2id to compare the password hash
3. Token Generation
The system generates two JWT tokens with base TTL + random jitter:
- • Access Token: 1h base (configurable) + up to
JWT_JITTER_SECONDS - • Refresh Token: 7d base (configurable) + the same jitter
{
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"user": {
"id": "123",
"email": "[email protected]",
"roles": ["admin", "user"]
}
}
4. Using the Token
The client includes the Access Token in the Authorization header of all requests
GET /api/v1/resources Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... Content-Type: application/json
5. Token Validation
JwtAuthGuard intercepts the request, extracts, and validates the token using the Passport JWT strategy
6. Refresh Token
When the Access Token expires, use the Refresh Token to obtain a new Access Token
POST /api/v1/auth/refresh-token
Content-Type: application/json
{
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
Verify token
Check whether the Access Token is still valid with POST /api/v1/auth/verify-token. Requires the Authorization: Bearer header.
POST /api/v1/auth/verify-token Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... Content-Type: application/json
{
"message": "Token is valid",
"user": {
"id": "123",
"email": "[email protected]",
"roles": ["admin", "user"]
}
}
JWT Jitter
Jitter is a random variation added to the token expiration time (TTL) at sign time. The goal is to spread exp across clients that log in at the same time and reduce simultaneous refresh spikes (thundering herd).
exp = now + baseTTL + random(0, JWT_JITTER_SECONDS)
Configuration
- • Variable:
JWT_JITTER_SECONDS - • Default: 60 seconds
- • Applied to access and refresh tokens
- •
0disables jitter
Practical example
With 1h base and 60 jitter:
- • Token A: expires in 3600s (1h + 0s)
- • Token B: expires in 3637s (1h + 37s)
- • Token C: expires in 3660s (1h + 60s)
Not clock skew
Jitter only changes TTL at token issuance. Passport validation continues with ignoreExpiration: false and no extra leeway.
Account creation
Register a user with POST /api/v1/auth/register. The account starts inactive until email confirmation; JWT tokens are already returned (with jitter).
POST /api/v1/auth/register
Content-Type: application/json
{
"firstName": "Jane",
"lastName": "Smith",
"username": "janesmith",
"email": "[email protected]",
"password": "Str0ng!P@ssword",
"confirmPassword": "Str0ng!P@ssword"
}
{
"user": {
"id": "uuid-...",
"firstName": "Jane",
"lastName": "Smith",
"username": "janesmith",
"email": "[email protected]",
"isActive": false,
"roles": []
},
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"message": "Registration successful. Please check your email to confirm your account."
}
Duplicate email / username
If the email (or username) is already registered, the API responds with 409 Conflict instead of 400.
{
"statusCode": 409,
"message": "A user with this email already exists",
"error": "Conflict"
}
Email verification
The verification link/token arrives via SMTP. In local Docker, open MailHog at /mailhog/. Confirm with GET /api/v1/auth/verify-email?token=....
Password reset
Two-step flow: request the recovery email, then set the new password with the received token.
1. Request reset
POST /api/v1/auth/forgot-password
Content-Type: application/json
{
"email": "[email protected]"
}
{
"message": "Password reset instructions sent to your email"
}
2. Set new password
Use the token from the email (in Docker: MailHog at /mailhog/).
POST /api/v1/auth/reset-password
Content-Type: application/json
{
"token": "abc123def456...",
"password": "NewStr0ng!P@ssword",
"confirmPassword": "NewStr0ng!P@ssword"
}
{
"message": "Password reset successfully"
}
Role System
Access is controlled by the Role enum. Each user can have multiple roles; the field is optional, but values must belong to the enum.
Role enum values
These values are defined in src/modules/auth/enums/role.enum.ts. For new roles, extend the enum — do not use free-form strings.
Super Administrator - Full system access
Administrator - User and resource management
Manager - Specific resource management
User - Access to basic features
Role structure in the model
The users module types roles as Role[]. See also the Users page.
import { Role } from '@modules/auth/enums/role.enum';
enum Role {
SUPER = 'super',
ADMIN = 'admin',
MANAGER = 'manager',
USER = 'user',
}
interface User {
id: string;
email: string;
roles: Role[]; // Role[] — enum values
// ... other fields
}
// User example
const user = {
id: "123",
email: "[email protected]",
roles: [Role.ADMIN, Role.USER] // valid Role enum values
};
Decorators and Guards
Available Decorators
@Public()- Make route public
Marks a route as public, allowing access without authentication. Useful for login, registration, and similar endpoints.
import { Public } from '@modules/auth/decorators/public.decorator';
@Controller('auth')
export class AuthController {
@Public() // Public route, no authentication required
@Post('login')
async login(@Body() loginDto: LoginDto) {
return this.authService.login(loginDto);
}
}
@Roles()- Restrict by role
Defines which roles are required to access the route. The user must have at least one of the specified roles.
import { Roles } from '@modules/auth/decorators/roles.decorator';
import { Role } from '@modules/auth/enums/role.enum';
@Controller('resources')
export class ResourceController {
@Roles(Role.ADMIN, Role.SUPER) // Role enum values
@Get()
async findAll() {
return this.resourceService.findAll();
}
@Roles(Role.ADMIN)
@Delete(':id')
async delete(@Param('id') id: string) {
return this.resourceService.delete(id);
}
}
Implemented Guards
JwtAuthGuard
Validates the JWT token on requests: checks that it is valid and not expired, and exposes the payload on req.user. Routes with @Public() are allowed.
RolesGuard
Validates that the authenticated user has at least one of the roles required by @Roles().
Practical Examples
Example 1: Public Route
@Controller('auth')
export class AuthController {
@Public() // No authentication required
@Post('register')
async register(@Body() registerDto: RegisterDto) {
return this.authService.register(registerDto);
}
}
Example 2: Protected Route (Any Authenticated User)
@Controller('resources')
export class ResourceController {
// Without @Public() → authentication required
// Without @Roles() → any authenticated user can access
@Get('me')
async getMine(@Request() req) {
return this.resourceService.findByOwner(req.user.id);
}
}
Example 3: Route with Specific Role
import { Role } from '@modules/auth/enums/role.enum';
@Controller('resources')
export class ResourceController {
@Roles(Role.SUPER) // Role enum values
@Get()
async findAll() {
return this.resourceService.findAll();
}
@Roles(Role.ADMIN)
@Delete(':id')
async delete(@Param('id') id: string) {
return this.resourceService.delete(id);
}
}
Example 4: Multiple Roles
import { Role } from '@modules/auth/enums/role.enum';
@Controller('resources')
export class ResourceController {
// User must have Role.ADMIN OR Role.MANAGER (OR)
@Roles(Role.ADMIN, Role.MANAGER)
@Get('summary')
async getSummary() {
return this.resourceService.getSummary();
}
// @Roles() uses OR, not AND
// For AND, create a custom guard
@Roles(Role.SUPER)
@Get('audit')
async getAudit() {
return this.resourceService.getAudit();
}
}
Token Payload
The JWT token carries the payload below after authentication. The data is available on req.user.
{
"id": "user123",
"email": "[email protected]",
"roles": ["admin", "user"],
"iat": 1234567890, // Issued at
"exp": 1234571490 // Expiration
}
Configuration
JWT settings are configured through environment variables and accessed via ConfigService.
# JWT Configuration JWT_SECRET=seu_jwt_secret_super_seguro_aqui JWT_EXPIRATION_TIME=1h JWT_REFRESH_SECRET=seu_refresh_secret_super_seguro JWT_REFRESH_EXPIRATION_TIME=7d JWT_JITTER_SECONDS=60 # Argon2id Configuration ARGON2_MEMORY_COST=19456 ARGON2_TIME_COST=2 ARGON2_PARALLELISM=1
Security
Use strong, unique secrets in production. JWT_SECRET should be a long, random string. Never commit secrets to the repository.
Social authenticationOptional
Google, Facebook, X / Twitter, GitHub, Figma, LinkedIn, Slack, Atlassian, GitLab, Bitbucket, Discord, Twitch, and Reddit OAuth2 use Authorization Code Exchange — JWTs are never placed in the redirect URL. Steam uses OpenID 2.0 (not OAuth2) with the same exchange-code flow; Steam and Reddit never return email. Full setup, API reference, and web/mobile examples live under Social Authentication.