From 093292d9c9edeb0b9ba9f6559a0802975d551394 Mon Sep 17 00:00:00 2001 From: Rafael Alves Lopes Date: Fri, 8 May 2026 17:12:37 -0300 Subject: [PATCH] Adicionar Auth --- Auth.md | 260 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 260 insertions(+) create mode 100644 Auth.md diff --git a/Auth.md b/Auth.md new file mode 100644 index 0000000..36ea8c4 --- /dev/null +++ b/Auth.md @@ -0,0 +1,260 @@ +# 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 + +```env +# 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: + +```json +{ + "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`: + +```typescript +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 { + // 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 } + } +} +``` + +2. Registre o provider em `auth.module.ts`: + +```typescript +providers: [ + AuthConfigService, + AuthService, + AuthTokenService, + LdapAuthProvider, + MicrosoftOAuthProvider, + OAuthStateService, + NovoProvedorProvider, // adicione aqui +], +``` + +3. Injete no `AuthService` e exponha o método necessário: + +```typescript +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); +} +``` + +4. Adicione a rota no `AuthController`. + +5. 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 ` + +### `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) \ No newline at end of file