Skip to main content

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

1

User submits credentials

User enters email and password on the login page
2

Frontend sends request

Next.js frontend sends credentials to the backend via NextAuth
3

Backend validates credentials

NestJS validates the email and password using LocalStrategy
4

Password verification

Password is hashed with bcrypt and compared to stored hash
5

JWT token generation

If valid, backend generates a signed JWT token
6

Token returned to client

JWT token is returned to frontend and stored in session
7

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

sub
string
User ID (UUID)
email
string
User email address
access
number
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

Login Form Component

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

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

Build docs developers (and LLMs) love