Authentication and Authorization

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

HTTP RequestPOST /api/v1/auth/login
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

HTTP Response200 OK
{
  "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

HTTP RequestWith Token
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

HTTP RequestPOST /api/v1/auth/refresh-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.

HTTP RequestPOST /api/v1/auth/verify-token
POST /api/v1/auth/verify-token
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Content-Type: application/json
HTTP Response200 OK
{
  "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).

FormulaEffective TTL
exp = now + baseTTL + random(0, JWT_JITTER_SECONDS)

Configuration

  • • Variable: JWT_JITTER_SECONDS
  • • Default: 60 seconds
  • • Applied to access and refresh tokens
  • 0 disables 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).

HTTP RequestPOST /api/v1/auth/register
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"
}
HTTP Response201 Created
{
  "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.

HTTP Response409 Conflict
{
  "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

HTTP RequestPOST /api/v1/auth/forgot-password
POST /api/v1/auth/forgot-password
Content-Type: application/json

{
  "email": "[email protected]"
}
HTTP Response200 OK
{
  "message": "Password reset instructions sent to your email"
}

2. Set new password

Use the token from the email (in Docker: MailHog at /mailhog/).

HTTP RequestPOST /api/v1/auth/reset-password
POST /api/v1/auth/reset-password
Content-Type: application/json

{
  "token": "abc123def456...",
  "password": "NewStr0ng!P@ssword",
  "confirmPassword": "NewStr0ng!P@ssword"
}
HTTP Response200 OK
{
  "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

Super Administrator - Full system access

admin

Administrator - User and resource management

manager

Manager - Specific resource management

user

User - Access to basic features

Role structure in the model

The users module types roles as Role[]. See also the Users page.

TypeScriptRole enum + User
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.

TypeScriptController
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.

TypeScriptController
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

TypeScriptPublic 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)

TypeScriptProtected Route
@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

TypeScriptRole-Based Route
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

TypeScriptMultiple 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.

JSONJWT Payload
{
  "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.

Environment Variables.env
# 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.