omnichannel-backend/docs/auth.md
Rafael Lopes 5bd13e30f1 FEAT: Implementa módulo de autenticação com JWT
* Bootstrap da aplicação com NestJS + TypeScript
* Migração de Node.js puro + JavaScript para NestJS como framework
* Estrutura base: AppModule, AppController, health check em /health
* loadEnv com busca hierárquica de .env por ambiente
* Módulo auth completo com arquitetura em camadas:
  - AuthController: rotas HTTP de autenticação
  - AuthService: fachada de negócio
  - AuthConfigService: leitura centralizada de variáveis de ambiente
  - AuthTokenService: emissão de JWT próprio da aplicação
* Autenticação via LDAP/Active Directory com ldapts
* Autenticação via Microsoft OAuth 2.0 (Entra ID)
* Proteção CSRF no fluxo OAuth com HMAC state assinado
* Endpoint /auth/config para o frontend descobrir provedores ativos
* Documentação do módulo em docs/auth.md
2026-05-08 17:10:50 -03:00

8.4 KiB

Módulo de Autenticação

Visão geral

O módulo auth centraliza toda a lógica de autenticação do Omnichannel. Ele suporta múltiplos provedores de identidade e emite JWT próprio da aplicação, independente de qual provedor foi usado.

Provedores implementados:

  • LDAP / Active Directory — login com usuário e senha do AD corporativo
  • Microsoft OAuth (Entra ID) — login via conta Microsoft com redirect OAuth 2.0

A arquitetura foi desenhada para facilitar a adição de novos provedores no futuro.


Estrutura de arquivos

src/modules/auth/
├── auth.module.ts          # Registro do módulo no NestJS
├── auth.controller.ts      # Rotas HTTP
├── auth.service.ts         # Fachada — delega para os providers
├── auth.config.ts          # Leitura de variáveis de ambiente
├── auth-token.service.ts   # Emissão de JWT da aplicação
├── auth.types.ts           # Interfaces TypeScript compartilhadas
└── providers/
    ├── ldap-auth.provider.ts       # Autenticação LDAP/AD
    ├── microsoft-oauth.provider.ts # Autenticação Microsoft OAuth
    └── oauth-state.service.ts      # Proteção CSRF para OAuth

Rotas disponíveis

Método Rota Descrição
GET /auth/config Retorna quais provedores estão habilitados
POST /auth/login Login com usuário e senha (LDAP/AD)
GET /auth/oauth/microsoft/start Inicia o fluxo OAuth com a Microsoft
GET /auth/oauth/microsoft/callback Callback que a Microsoft chama após o login

Variáveis de ambiente

# Servidor
PORT=3001
FRONTEND_URL=http://localhost:3000

# JWT
JWT_SECRET=uma-chave-longa-e-aleatoria
JWT_EXPIRES_IN=8h

# Provedores ativos (separados por vírgula)
AUTH_PROVIDERS=ldap

# LDAP / Active Directory
LDAP_ENABLED=true
LDAP_URL=ldaps://servidor-ad:636
LDAP_DOMAIN=empresa.com.br
LDAP_USER_DN_TEMPLATE={{username}}@empresa.com.br
LDAP_SEARCH_BASE=DC=empresa,DC=com
LDAP_SEARCH_FILTER=(&(objectClass=user)(sAMAccountName={{username}}))
LDAP_TIMEOUT_MS=5000

# Microsoft Entra ID (desabilitado por padrão)
MICROSOFT_ENABLED=false
MICROSOFT_TENANT_ID=common
MICROSOFT_CLIENT_ID=
MICROSOFT_CLIENT_SECRET=
MICROSOFT_REDIRECT_URI=http://localhost:3001/auth/oauth/microsoft/callback
MICROSOFT_SUCCESS_REDIRECT_URL=http://localhost:3000/login

JWT_SECRET deve ser uma string longa e aleatória. Em produção, nunca use o valor padrão do .env.development.


Fluxo LDAP / Active Directory

Frontend
  → POST /auth/login  { username, password }
  → AuthController
  → AuthService.loginWithLdap()
  → LdapAuthProvider.authenticate()
      → Conecta no servidor AD (LDAP_URL)
      → Faz bind com o usuário e senha
          → Se o bind falhar: UnauthorizedException
      → Busca dados do usuário no diretório (se LDAP_SEARCH_BASE configurado)
      → Monta objeto AuthenticatedUser
  → AuthTokenService.issueToken()
      → Gera JWT assinado com JWT_SECRET
  → Retorna { token, user } para o frontend

O AD apenas valida a identidade. O JWT emitido é da aplicação, não do AD.


Fluxo Microsoft OAuth

1. Frontend redireciona para GET /auth/oauth/microsoft/start
   → Backend gera um state assinado (proteção CSRF)
   → Backend redireciona para login.microsoftonline.com

2. Usuário autentica na Microsoft

3. Microsoft chama GET /auth/oauth/microsoft/callback?code=...&state=...
   → Backend valida o state (assinatura + expiração)
   → Backend troca o code por access_token (chamada server-to-server)
   → Backend consulta Microsoft Graph /me para obter dados do usuário
   → AuthTokenService.issueToken() gera JWT próprio
   → Backend redireciona para MICROSOFT_SUCCESS_REDIRECT_URL?token=...

4. Frontend salva o token e navega para /home

Proteção CSRF com OAuth State

O OAuthStateService protege o fluxo OAuth contra ataques de CSRF.

Como funciona:

  1. No início do fluxo, o backend cria um state:

    • Gera um nonce aleatório + timestamp
    • Converte para base64url
    • Assina com HMAC-SHA256 usando o JWT_SECRET
    • Formato final: payload.assinatura
  2. No callback, o backend verifica:

    • O state tem os dois pedaços (payload.assinatura)
    • A assinatura é válida (recalcula e compara com timingSafeEqual)
    • O state não expirou (padrão: 10 minutos, configurável via MICROSOFT_STATE_MAX_AGE_MS)

Se qualquer verificação falhar, o callback é rejeitado com 400 Bad Request.


JWT da aplicação

Após qualquer autenticação bem-sucedida, o AuthTokenService emite um JWT com o seguinte payload:

{
  "sub": "identificador-do-usuario",
  "name": "Nome Completo",
  "email": "usuario@empresa.com",
  "provider": "ldap",
  "username": "usuario"
}

O sub é atualmente o email ou identificador externo. Quando houver banco de dados, deve ser substituído pelo ID interno da tabela users.


Como adicionar um novo provedor

  1. Crie o arquivo em src/modules/auth/providers/novo-provedor.provider.ts:
import { Injectable } from '@nestjs/common';
import { AuthConfigService } from '../auth.config';
import { AuthTokenService } from '../auth-token.service';
import { AuthResult } from '../auth.types';

@Injectable()
export class NovoProvedorProvider {
  constructor(
    private readonly authConfig: AuthConfigService,
    private readonly authToken: AuthTokenService,
  ) {}

  async authenticate(/* dados necessários */): Promise<AuthResult> {
    // 1. Valide as credenciais no provedor externo
    // 2. Monte o objeto AuthenticatedUser
    // 3. Emita o token com this.authToken.issueToken(user)
    // 4. Retorne { token, user }
  }
}
  1. Registre o provider em auth.module.ts:
providers: [
  AuthConfigService,
  AuthService,
  AuthTokenService,
  LdapAuthProvider,
  MicrosoftOAuthProvider,
  OAuthStateService,
  NovoProvedorProvider, // adicione aqui
],
  1. Injete no AuthService e exponha o método necessário:
constructor(
  private readonly authConfig: AuthConfigService,
  private readonly ldapAuthProvider: LdapAuthProvider,
  private readonly microsoftOAuthProvider: MicrosoftOAuthProvider,
  private readonly novoProvedorProvider: NovoProvedorProvider, // injete aqui
) {}

loginComNovoProvedor(dados: any) {
  return this.novoProvedorProvider.authenticate(dados);
}
  1. Adicione a rota no AuthController.

  2. Se o provedor precisar de configuração, adicione as variáveis no AuthConfigService e no .env.


Diagnóstico de problemas

Login LDAP falha com UnauthorizedException

  • Verifique se LDAP_URL está acessível a partir do servidor backend
  • Confirme que LDAP_DOMAIN ou LDAP_USER_DN_TEMPLATE está correto
  • Teste a conectividade: ldapsearch -H ldaps://servidor:636 -x
  • Verifique LDAP_TIMEOUT_MS — servidores lentos podem estar expirando
  • O erro é genérico intencionalmente para não vazar informações. Adicione um console.log(_error) temporário no catch do ldap-auth.provider.ts para ver o erro real

Login Microsoft falha com 400 Bad Request

  • O state expirou (padrão: 10 minutos). Se o usuário demorou muito na tela da Microsoft, repita o fluxo
  • Verifique se MICROSOFT_REDIRECT_URI no .env é idêntico ao cadastrado no Azure App Registration
  • Confirme que MICROSOFT_CLIENT_ID e MICROSOFT_CLIENT_SECRET estão corretos e não expiraram

Token inválido no frontend

  • Verifique se JWT_SECRET não mudou entre deploys — isso invalida todos os tokens emitidos anteriormente
  • Confirme que o frontend está enviando o header Authorization: Bearer <token>

GET /auth/config retorna os provedores errados

  • Verifique LDAP_ENABLED e MICROSOFT_ENABLED no .env
  • Reinicie o servidor — variáveis de ambiente são lidas na inicialização

O que ainda falta para produção

  • Tabela users no banco de dados
  • Tabela auth_identities para vincular provedores externos ao usuário interno
  • sub do JWT usando ID interno do banco, não email externo
  • Guard NestJS para proteger rotas privadas (@UseGuards(AuthGuard))
  • Roles e permissões
  • Auditoria de login
  • Trocar token na query string por cookie HTTP-only (reduz exposição no browser)