Overview
ARCA uses a JWT (JSON Web Token) based authentication system built with NestJS on the backend and Next.js with NextAuth on the frontend. The system provides secure login, session management, and route protection.
Authentication is implemented using Passport.js strategies for both local (email/password) and JWT-based authentication.
Authentication Flow
User submits credentials
User enters email and password on the login page
Frontend sends request
Next.js frontend sends credentials to the backend via NextAuth
Backend validates credentials
NestJS validates the email and password using LocalStrategy
Password verification
Password is hashed with bcrypt and compared to stored hash
JWT token generation
If valid, backend generates a signed JWT token
Token returned to client
JWT token is returned to frontend and stored in session
Subsequent requests
Frontend includes JWT token in Authorization header for authenticated requests
Backend Authentication
Local Strategy (Login)
The local strategy handles email/password authentication:
apps/backend/src/auth/local.strategy.ts
import { Strategy } from 'passport-local' ;
import { PassportStrategy } from '@nestjs/passport' ;
import { Injectable , UnauthorizedException } from '@nestjs/common' ;
import { AuthService } from './auth.service' ;
@ Injectable ()
export class LocalStrategy extends PassportStrategy ( Strategy ) {
constructor ( private authService : AuthService ) {
super ({ usernameField: 'email' });
}
async validate ( email : string , password : string ) : Promise < any > {
const user = await this . authService . validateUser ({ email , password });
if ( ! user ) {
throw new UnauthorizedException ();
}
return user ;
}
}
The strategy is configured to use email as the username field instead of the default “username” field.
User Validation
The AuthService validates credentials and returns user data:
apps/backend/src/auth/auth.service.ts
async validateUser ( body : LoginDto ): Promise < any > {
const user = await this . prisma . usuario . findFirst ({
where: {
email: body . email ,
},
});
if (! user ) {
throw new UnauthorizedException ( 'Senha ou e-mail inválido.' );
}
const isPasswordValid = await this . hashingService . compare (
body . password ,
user . senhaHash ,
);
if (! isPasswordValid ) {
throw new UnauthorizedException ( 'Senha ou e-mail inválido.' );
}
// Retorna o usuário sem a senha
const { senhaHash , ... result } = user;
return result;
}
Password hashes are never returned to the client. The senhaHash field is explicitly excluded from response objects.
Password Hashing with Bcrypt
ARCA uses bcrypt for secure password hashing:
apps/backend/src/auth/hash/bcrypt.service.ts
import * as bcrypt from 'bcrypt' ;
export class BcryptService extends HashingServiceProtocol {
async hash ( password : string ) : Promise < string > {
const salt = await bcrypt . genSalt ();
return bcrypt . hash ( password , salt );
}
async compare ( password : string , hash : string ) : Promise < boolean > {
return bcrypt . compare ( password , hash );
}
}
Bcrypt automatically generates a unique salt for each password, providing protection against rainbow table attacks.
JWT Token Generation
After successful validation, a JWT token is generated:
apps/backend/src/auth/auth.service.ts
async login ( user : UserDto ): Promise < any > {
const token = await this . jwtService . signAsync (
{
sub: user . id_User ,
email: user . email ,
access: user . roleId ,
},
{
secret: this . jwtConfiguration . secret ,
expiresIn: this . jwtConfiguration . jwtTtl ,
audience: this . jwtConfiguration . audience ,
issuer: this . jwtConfiguration . issuer ,
},
);
return {
id : user . id_User ,
name : user . nome ,
email : user . email ,
roleId : user . roleId ,
token : token ,
};
}
JWT Payload Structure
User role ID (1=Admin, 2=Secretário, 3=Supervisor, 4=Estagiário)
Login Controller
The login endpoint is protected by the LocalAuthGuard:
apps/backend/src/auth/auth.controller.ts
import { Controller , Post , UseGuards , Request } from '@nestjs/common' ;
import { AuthService } from './auth.service' ;
import { LocalAuthGuard } from './guards/local-auth.guard' ;
@ Controller ( 'auth' )
export class AuthController {
constructor ( private authService : AuthService ) {}
@ UseGuards ( LocalAuthGuard )
@ Post ( 'login' )
async loginWithPassport (@ Request () req : any ) : Promise < any > {
// Depois de verificado, geramos um token para o usuário
return this . authService . login ( req . user );
}
}
JWT Strategy (Protected Routes)
The JWT strategy validates tokens on subsequent requests:
apps/backend/src/auth/jwt.strategy.ts
import { ExtractJwt , Strategy } from 'passport-jwt' ;
import { PassportStrategy } from '@nestjs/passport' ;
import { Injectable } from '@nestjs/common' ;
@ Injectable ()
export class JwtStrategy extends PassportStrategy ( Strategy ) {
constructor ( private readonly authService : AuthService ) {
super ({
jwtFromRequest: ExtractJwt . fromAuthHeaderAsBearerToken (),
ignoreExpiration: false ,
secretOrKey: jwtConstants . secret as string ,
});
}
async validate ( payload ) {
return payload ;
}
}
Tokens are extracted from the Authorization header using the Bearer scheme: Authorization: Bearer <token>
Protecting API Routes
Use the JwtAuthGuard to protect routes:
apps/backend/src/users/users.controller.ts
import { Controller , Get , UseGuards } from '@nestjs/common' ;
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard' ;
@ Controller ( 'users' )
@ UseGuards ( JwtAuthGuard ) // Protects all routes in this controller
export class UsersController {
@ Get ()
findAll (@ Req () req : any ) {
// req.user contains the decoded JWT payload
return this . usersService . findAll ( req . user );
}
}
Frontend Authentication
The login form uses NextAuth for authentication:
apps/frontend/components/login-form.tsx
import { signIn } from "next-auth/react"
import { useState } from "react"
import { useRouter } from "next/navigation"
export function LoginForm () {
const [ email , setEmail ] = useState ( "" )
const [ password , setPassword ] = useState ( "" )
const [ isLoading , setIsLoading ] = useState ( false )
const [ error , setError ] = useState ( "" )
const router = useRouter ()
const handleSubmit = async ( e : React . FormEvent ) => {
e . preventDefault ()
setIsLoading ( true )
setError ( "" )
try {
const result = await signIn ( "credentials" , {
email ,
password ,
redirect: false ,
})
if ( result ?. error ) {
setError ( "Credenciais inválidas. Verifique seu email e senha." )
} else {
router . push ( "/" )
router . refresh ()
}
} catch ( error ) {
setError ( "Erro ao fazer login. Tente novamente." )
} finally {
setIsLoading ( false )
}
}
// ... form JSX
}
Login Page
The login page redirects authenticated users:
apps/frontend/app/login/page.tsx
import { useSession } from "next-auth/react" ;
import { useRouter } from "next/navigation" ;
import { useEffect } from "react" ;
export default function LoginPage () {
const { data : session , status } = useSession ();
const router = useRouter ();
useEffect (() => {
if ( status === "authenticated" ) {
router . push ( "/dashboard" );
router . refresh ();
}
}, [ status , router ]);
if ( status === "loading" ) {
return < div > Carregando... </ div > ;
}
if ( status === "authenticated" ) {
return < div > Redirecionando... </ div > ;
}
return < LoginForm /> ;
}
Protected Routes Middleware
Middleware enforces authentication and role-based access:
apps/frontend/middleware.ts
import { withAuth } from "next-auth/middleware"
import { NextResponse } from "next/server"
export default withAuth (
function middleware ( req ) {
const token = req . nextauth . token
const pathname = req . nextUrl . pathname
// Define as permissões mínimas para cada rota
const routePermissions : Record < string , number > = {
"/dashboard/usuarios" : 2 , // Secretário (2) ou superior (Admin = 1)
"/dashboard/usuarios/criar" : 2 ,
"/dashboard/usuarios/permissoes" : 1 , // Apenas Admin
}
// Verifica se a rota tem restrições
for ( const [ route , maxRoleId ] of Object . entries ( routePermissions )) {
if ( pathname . startsWith ( route )) {
const userRoleId = token ?. roleId as number
if ( ! userRoleId || userRoleId > maxRoleId ) {
return NextResponse . redirect ( new URL ( '/dashboard/unauthorized' , req . url ))
}
}
}
return NextResponse . next ()
} ,
{
callbacks: {
authorized : ({ token }) => !! token
} ,
}
)
export const config = {
matcher: [ "/dashboard/:path*" ]
}
The middleware protects all routes under /dashboard/*. Users must be authenticated to access any dashboard page.
Session Management
Accessing User Session
Use the useSession hook to access the current user:
import { useSession } from "next-auth/react" ;
function MyComponent () {
const { data : session , status } = useSession ();
if ( status === "loading" ) {
return < div > Loading... </ div > ;
}
if ( status === "unauthenticated" ) {
return < div > Please log in </ div > ;
}
return (
< div >
< p > Logged in as: { session . user . email } </ p >
< p > Role ID: { session . user . roleId } </ p >
</ div >
);
}
Logging Out
import { signOut } from "next-auth/react" ;
function LogoutButton () {
return (
< button onClick = { () => signOut () } >
Logout
</ button >
);
}
Security Best Practices
Always use HTTPS to protect JWT tokens in transit. Tokens sent over HTTP can be intercepted by attackers.
Set Appropriate Token Expiration
Configure JWT expiration times based on your security requirements. Shorter expirations (e.g., 1 hour) are more secure but require more frequent re-authentication.
Store JWT secrets in environment variables, never commit them to version control. Use strong, randomly generated secrets.
Add rate limiting to the login endpoint to prevent brute force attacks.
Enforce strong password requirements:
Minimum 8 characters (enforced by validation)
Mix of uppercase, lowercase, numbers, and special characters
Password complexity checks
Consider implementing account lockout after multiple failed login attempts.
Common Authentication Errors
UnauthorizedException: Senha ou e-mail inválido
Cause : Incorrect email or password.Solution : Verify credentials. This generic message prevents attackers from knowing whether the email exists.
Cause : The JWT token has exceeded its expiration time.Solution : User must log in again to obtain a new token.
Cause : Token has been tampered with or JWT secret has changed.Solution : User must log in again. Verify JWT secret configuration.
Cause : Request missing Authorization header.Solution : Ensure frontend includes JWT token in requests: Authorization: Bearer <token>
Testing Authentication
Testing Login
# Login request
curl -X POST http://localhost:3000/auth/login \
-H "Content-Type: application/json" \
-d '{
"email": "[email protected] ",
"password": "Estagiario123!"
}'
Response:
{
"id" : "550e8400-e29b-41d4-a716-446655440000" ,
"name" : "Estagiário Padrão" ,
"email" : "[email protected] " ,
"roleId" : 4 ,
"token" : "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
Testing Protected Endpoints
# Use the token from login response
curl -X GET http://localhost:3000/users \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
Default User Accounts
Change default passwords immediately in production! These accounts are created during database seeding for development purposes.
Role Email Password Admin [email protected] Admin123! Secretário [email protected] Secretario123! Supervisor [email protected] Supervisor123! Estagiário [email protected] Estagiario123!
Roles & Permissions Learn about the role-based access control system
User Accounts Create and manage user accounts