Atualizada wiki de autenticação e acesso

Rafael Alves Lopes 2026-05-29 12:22:37 -03:00
parent e51129a1dd
commit 4a83fb45d1
2 changed files with 351 additions and 330 deletions

@ -30,7 +30,8 @@ Frontend
-> Backend cria/atualiza usuarios_provedores -> Backend cria/atualiza usuarios_provedores
-> Backend consulta usuarios_perfis e usuarios_areas -> Backend consulta usuarios_perfis e usuarios_areas
-> Backend emite JWT com perfis/areas -> Backend emite JWT com perfis/areas
-> Frontend salva authToken/authUser -> Frontend salva authToken/authUser em sessionStorage
-> Frontend envia Authorization: Bearer <token> nas rotas privadas
-> Frontend navega para /home -> Frontend navega para /home
``` ```
@ -298,10 +299,21 @@ http://localhost:3001/admin/access/users
### Frontend sem depender de AD ### Frontend sem depender de AD
No console do navegador: Para testes manuais antigos era comum simular sessao no navegador. Esse caminho nao representa mais o fluxo real porque a API agora valida JWT assinado.
O caminho recomendado e:
1. criar o usuario real no AD ou Microsoft;
2. fazer login pela tela;
3. atribuir perfil/area pelo Admin;
4. repetir login ou atualizar a sessao.
Para teste local sem provedor corporativo, gere um JWT valido pelo backend ou use um usuario real de desenvolvimento.
Exemplo apenas para testar renderizacao visual do frontend, sem acesso real a API privada:
```js ```js
localStorage.setItem('authUser', JSON.stringify({ sessionStorage.setItem('authUser', JSON.stringify({
username: 'admin@sothis.com.br', username: 'admin@sothis.com.br',
name: 'Admin Demo', name: 'Admin Demo',
email: 'admin@sothis.com.br', email: 'admin@sothis.com.br',
@ -310,14 +322,14 @@ localStorage.setItem('authUser', JSON.stringify({
areas: ['Suporte'], areas: ['Suporte'],
accessStatus: 'assigned' accessStatus: 'assigned'
})); }));
localStorage.setItem('authToken', 'dev-token'); sessionStorage.setItem('authToken', 'token-jwt-valido-gerado-pelo-backend');
location.href = '/home'; location.href = '/home';
``` ```
Para usuario sem atribuicao: Para usuario sem atribuicao:
```js ```js
localStorage.setItem('authUser', JSON.stringify({ sessionStorage.setItem('authUser', JSON.stringify({
username: 'novo.usuario', username: 'novo.usuario',
name: 'Novo Usuario', name: 'Novo Usuario',
email: 'novo.usuario@sothis.com.br', email: 'novo.usuario@sothis.com.br',
@ -326,7 +338,7 @@ localStorage.setItem('authUser', JSON.stringify({
areas: [], areas: [],
accessStatus: 'unassigned' accessStatus: 'unassigned'
})); }));
localStorage.setItem('authToken', 'dev-token'); sessionStorage.setItem('authToken', 'token-jwt-valido-gerado-pelo-backend');
location.href = '/home'; location.href = '/home';
``` ```
@ -334,7 +346,7 @@ location.href = '/home';
## Limitacoes atuais ## Limitacoes atuais
- Os endpoints `/admin/access/*` ainda nao possuem guard JWT nem checagem de perfil Admin. - Os endpoints `/admin/access/*` exigem JWT. Rotas sensiveis exigem perfil `Admin`; visoes operacionais podem aceitar `Admin` ou `Supervisor`.
- A alteracao de acesso substitui perfil/area atuais por um unico perfil e uma unica area principal. - A alteracao de acesso substitui perfil/area atuais por um unico perfil e uma unica area principal.
- Ainda nao ha auditoria das alteracoes de acesso. - Ainda nao ha auditoria das alteracoes de acesso.
- O painel Admin consome os endpoints reais, mas ainda possui fallback visual mockado se o backend estiver indisponivel. - O painel Admin consome os endpoints reais, mas ainda possui fallback visual mockado se o backend estiver indisponivel.
@ -344,8 +356,6 @@ location.href = '/home';
## Proximos passos sugeridos ## Proximos passos sugeridos
- Adicionar `AuthGuard` JWT.
- Proteger `/admin/access/*` para `Admin`.
- Registrar auditoria em `logs_auditoria`. - Registrar auditoria em `logs_auditoria`.
- Criar endpoints CRUD completos para `areas`. - Criar endpoints CRUD completos para `areas`.
- Permitir multiplas areas por usuario na UI, mantendo uma area principal. - Permitir multiplas areas por usuario na UI, mantendo uma area principal.

413
Auth.md

@ -1,155 +1,165 @@
# Módulo de Autenticação # Modulo de Autenticacao
## Visão geral ## Visao 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. O modulo `auth` centraliza login, emissao de JWT, validacao de token, autorizacao por perfil e sincronizacao do usuario autenticado com o banco do Omnichannel.
Provedores implementados: Provedores implementados:
- **LDAP / Active Directory** — login com usuário e senha do AD corporativo - **LDAP / Active Directory**: login com usuario e senha corporativos.
- **Microsoft OAuth (Entra ID)** — login via conta Microsoft com redirect OAuth 2.0 - **Microsoft OAuth / Entra ID**: login via conta Microsoft.
A arquitetura foi desenhada para facilitar a adição de novos provedores no futuro. Depois que o provedor confirma a identidade, o backend emite um JWT proprio da aplicacao. Esse token deve ser enviado pelo frontend em todas as rotas privadas usando:
```http
Authorization: Bearer <token>
```
--- ---
## Estrutura de arquivos ## Estrutura de arquivos
``` ```text
src/modules/auth/ src/modules/auth/
├── auth.module.ts # Registro do módulo no NestJS |-- auth.module.ts
├── auth.controller.ts # Rotas HTTP |-- auth.controller.ts
├── auth.service.ts # Fachada — delega para os providers |-- auth.service.ts
├── auth.config.ts # Leitura de variáveis de ambiente |-- auth.config.ts
├── auth-token.service.ts # Emissão de JWT da aplicação |-- auth-token.service.ts
├── user-access.service.ts # Sincronização do usuário autenticado com o banco |-- auth.types.ts
├── auth.types.ts # Interfaces TypeScript compartilhadas |-- user-access.service.ts
└── providers/ |-- decorators/
├── ldap-auth.provider.ts # Autenticação LDAP/AD | |-- public.decorator.ts
├── microsoft-oauth.provider.ts # Autenticação Microsoft OAuth | `-- roles.decorator.ts
└── oauth-state.service.ts # Proteção CSRF para OAuth |-- dto/
| `-- login.dto.ts
|-- guards/
| |-- jwt-auth.guard.ts
| `-- roles.guard.ts
|-- providers/
| |-- ldap-auth.provider.ts
| |-- microsoft-oauth.provider.ts
| `-- oauth-state.service.ts
`-- repositories/
`-- user-access.repository.ts
``` ```
Responsabilidades principais:
- `AuthController`: expoe rotas publicas de login e OAuth.
- `AuthService`: fachada que delega para LDAP ou Microsoft.
- `AuthTokenService`: emite e valida JWT.
- `JwtAuthGuard`: exige Bearer token nas rotas privadas.
- `RolesGuard`: valida perfis com `@Roles()`.
- `UserAccessService`: regra de sincronizacao do usuario autenticado.
- `UserAccessRepository`: instrucoes SQL do fluxo de acesso.
- `LoginDto`: validacao formal do payload de login.
--- ---
## Rotas disponíveis ## Rotas publicas
| Método | Rota | Descrição | As rotas abaixo nao exigem JWT:
|--------|---------------------------------|------------------------------------------------|
| GET | `/auth/config` | Retorna quais provedores estão habilitados | | Metodo | Rota | Descricao |
| POST | `/auth/login` | Login com usuário e senha (LDAP/AD) | |---|---|---|
| GET | `/auth/oauth/microsoft/start` | Inicia o fluxo OAuth com a Microsoft | | GET | `/health` | Health check da API |
| GET | `/auth/oauth/microsoft/callback`| Callback que a Microsoft chama após o login | | GET | `/auth/config` | Informa provedores de login habilitados |
| POST | `/auth/login` | Login LDAP/AD |
| GET | `/auth/oauth/microsoft/start` | Inicia login Microsoft |
| GET | `/auth/oauth/microsoft/callback` | Recebe callback Microsoft |
Todas as demais rotas HTTP exigem JWT valido.
--- ---
## Variáveis de ambiente ## Rotas privadas e perfis
```env O `JwtAuthGuard` foi registrado como guard global. Isso significa que toda rota e privada por padrao, exceto as marcadas com `@Public()`.
# Servidor
PORT=3001
FRONTEND_URL=http://localhost:3000
# JWT Exemplo:
JWT_SECRET=uma-chave-longa-e-aleatoria
JWT_EXPIRES_IN=8h
# Provedores ativos (separados por vírgula) ```typescript
AUTH_PROVIDERS=ldap @Public()
@Get('config')
# LDAP / Active Directory getConfig() {}
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`. Para autorizacao por perfil, o backend usa `@Roles()`:
```typescript
@Roles('Admin')
@Get('users')
listUsers() {}
```
Regras atuais:
- `Admin`: acessa administracao, usuarios, areas, base de conhecimento e configuracoes sensiveis.
- `Supervisor`: acessa visoes operacionais liberadas para supervisao.
- `Agente`: acessa fluxos autenticados sem permissao administrativa.
--- ---
## Fluxo LDAP / Active Directory ## Fluxo LDAP / Active Directory
``` ```text
Frontend Frontend
→ POST /auth/login { username, password } -> POST /auth/login { username, password }
→ AuthController -> LoginDto valida payload
→ AuthService.loginWithLdap() -> AuthController
→ LdapAuthProvider.authenticate() -> AuthService.loginWithLdap()
→ Conecta no servidor AD (LDAP_URL) -> LdapAuthProvider.authenticate()
→ Faz bind com o usuário e senha -> valida se LDAP esta habilitado
→ Se o bind falhar: UnauthorizedException -> conecta no servidor LDAP
→ Busca dados do usuário no diretório (se LDAP_SEARCH_BASE configurado) -> faz bind com usuario e senha
→ Monta objeto AuthenticatedUser -> busca dados do usuario no diretorio, se configurado
→ UserAccessService.syncAuthenticatedUser() -> monta AuthenticatedUser
→ Cria/atualiza usuarios e usuarios_provedores -> UserAccessService.syncAuthenticatedUser()
→ Carrega perfis, areas e area principal -> UserAccessRepository executa SQL
→ AuthTokenService.issueToken() -> cria/atualiza usuarios
→ Gera JWT assinado com JWT_SECRET -> cria/atualiza usuarios_provedores
→ Retorna { token, user } para o frontend -> carrega perfis e areas
-> AuthTokenService.issueToken()
-> retorna { token, user }
``` ```
O AD apenas valida a identidade. O JWT emitido é da aplicação, não do AD. O erro retornado ao usuario continua generico (`Autenticacao falhou`). O erro real e registrado no log interno do provider para diagnostico sem vazar detalhes sensiveis.
--- ---
## Fluxo Microsoft OAuth ## Fluxo Microsoft OAuth
```text
Frontend
-> GET /auth/oauth/microsoft/start
Backend
-> gera state assinado com HMAC-SHA256 usando JWT_SECRET
-> redireciona para Microsoft
Usuario
-> autentica na Microsoft
Microsoft
-> chama /auth/oauth/microsoft/callback?code=...&state=...
Backend
-> valida state
-> troca code por access_token
-> consulta Microsoft Graph /me
-> sincroniza usuario no banco
-> emite JWT da aplicacao
-> redireciona para o frontend com payload no fragmento da URL
Frontend
-> le #auth=...
-> salva sessao em sessionStorage
-> limpa a URL
``` ```
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 O token nao e mais enviado em query string. Ele vai no fragmento `#auth=...`, que nao e enviado ao servidor em requisicoes HTTP e e removido pelo frontend apos processamento.
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
→ UserAccessService.syncAuthenticatedUser() sincroniza o usuário no banco
→ 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 ## JWT da aplicacao
O `OAuthStateService` protege o fluxo OAuth contra ataques de CSRF. Payload principal:
**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 ```json
{ {
@ -166,134 +176,135 @@ Após qualquer autenticação bem-sucedida, o `AuthTokenService` emite um JWT co
} }
``` ```
O `sub` usa o ID interno da tabela `usuarios`, convertido para string. O mesmo ID também é retornado no objeto de usuário como `databaseId`. O `sub` usa o ID interno da tabela `usuarios`.
O JWT é emitido e salvo no frontend, mas ainda falta a camada de `AuthGuard` no NestJS para validar o token nas rotas privadas. Portanto, hoje o token representa a sessão do usuário para o frontend, mas o backend ainda precisa ser endurecido para produção. O token e:
- assinado com `JWT_SECRET`;
- emitido com expiracao definida por `JWT_EXPIRES_IN`;
- validado em todas as rotas privadas;
- enviado pelo frontend como Bearer token.
--- ---
## Sincronização de usuário e acesso ## Sessao no frontend
Após o provedor autenticar o usuário, o `UserAccessService`: O frontend armazena `authToken` e `authUser` em `sessionStorage`, nao em `localStorage`.
1. faz upsert em `usuarios` usando email ou fallback `provider:username`; Motivo:
2. faz upsert em `usuarios_provedores`;
3. consulta perfis em `usuarios_perfis` + `perfis_acesso`;
4. consulta especialidades em `usuarios_areas` + `areas`;
5. retorna o usuário enriquecido com:
- `databaseId`;
- `perfis` / `profiles`;
- `areas`;
- `areaPrincipal`;
- `accessStatus`.
`accessStatus` fica como `assigned` quando o usuário possui perfil e área. Usuários sem vínculo suficiente entram como `unassigned` e caem na tela de pendência no frontend. - reduz persistencia indevida apos fechar o navegador;
- evita reaproveitamento de sessoes antigas salvas localmente;
- continua simples para o modelo atual do produto.
Observacao: uma alternativa ainda mais forte para producao seria cookie `HttpOnly` + `Secure` + `SameSite`, mas exigiria ajuste maior no fluxo do frontend e no CORS.
--- ---
## Como adicionar um novo provedor ## WebSocket WhatsApp
1. Crie o arquivo em `src/modules/auth/providers/novo-provedor.provider.ts`: O namespace Socket.IO `/whatsapp` tambem exige JWT no handshake.
```typescript O frontend envia:
import { Injectable } from '@nestjs/common';
import { AuthConfigService } from '../auth.config';
import { AuthTokenService } from '../auth-token.service';
import { AuthResult } from '../auth.types';
@Injectable() ```javascript
export class NovoProvedorProvider { io(WHATSAPP_SOCKET_URL, {
constructor( auth: {
private readonly authConfig: AuthConfigService, token: getAuthToken()
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 }
}
} }
});
``` ```
2. Registre o provider em `auth.module.ts`: Sem token valido, o socket nao conecta e nao recebe QR Code, status ou mensagens.
```typescript ---
providers: [
AuthConfigService, ## Rate limit
AuthService,
AuthTokenService, O backend usa `@nestjs/throttler` com limite global configuravel:
LdapAuthProvider,
MicrosoftOAuthProvider, ```env
OAuthStateService, RATE_LIMIT_TTL_MS=60000
NovoProvedorProvider, // adicione aqui RATE_LIMIT_MAX=300
],
``` ```
3. Injete no `AuthService` e exponha o método necessário: Padrao atual: 300 requisicoes por IP a cada 60 segundos.
```typescript ---
constructor(
private readonly authConfig: AuthConfigService,
private readonly ldapAuthProvider: LdapAuthProvider,
private readonly microsoftOAuthProvider: MicrosoftOAuthProvider,
private readonly novoProvedorProvider: NovoProvedorProvider, // injete aqui
) {}
loginComNovoProvedor(dados: any) { ## Variaveis de ambiente
return this.novoProvedorProvider.authenticate(dados);
} ```env
FRONTEND_URL=http://localhost:3000
JWT_SECRET=uma-chave-longa-e-aleatoria
JWT_EXPIRES_IN=8h
RATE_LIMIT_TTL_MS=60000
RATE_LIMIT_MAX=300
AUTH_PROVIDERS=ldap,microsoft
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_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
``` ```
4. Adicione a rota no `AuthController`. `JWT_SECRET` deve ser forte e exclusivo por ambiente.
5. Se o provedor precisar de configuração, adicione as variáveis no `AuthConfigService` e no `.env`.
--- ---
## Diagnóstico de problemas ## Diagnostico
### Login LDAP falha com `UnauthorizedException` ### Login LDAP falha
- Verifique se `LDAP_URL` está acessível a partir do servidor backend - Verifique `LDAP_URL`, `LDAP_DOMAIN` e `LDAP_USER_DN_TEMPLATE`.
- Confirme que `LDAP_DOMAIN` ou `LDAP_USER_DN_TEMPLATE` está correto - Confirme conectividade do backend com o AD.
- Teste a conectividade: `ldapsearch -H ldaps://servidor:636 -x` - Verifique se `LDAP_SEARCH_BASE` e `LDAP_SEARCH_FILTER` batem com o diretorio.
- Verifique `LDAP_TIMEOUT_MS` — servidores lentos podem estar expirando - Consulte logs do backend; o provider registra o motivo interno sem expor para o usuario.
- 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` ### Token invalido ou expirado
- O state expirou (padrão: 10 minutos). Se o usuário demorou muito na tela da Microsoft, repita o fluxo - Verifique se o frontend esta enviando `Authorization: Bearer <token>`.
- Verifique se `MICROSOFT_REDIRECT_URI` no `.env` é idêntico ao cadastrado no Azure App Registration - Verifique se `JWT_SECRET` nao mudou depois da emissao do token.
- Confirme que `MICROSOFT_CLIENT_ID` e `MICROSOFT_CLIENT_SECRET` estão corretos e não expiraram - Faca login novamente para renovar a sessao.
### Token inválido no frontend ### Usuario autenticado cai como `unassigned`
- Verifique se `JWT_SECRET` não mudou entre deploys — isso invalida todos os tokens emitidos anteriormente - O login funcionou, mas faltam perfil/area no banco.
- Confirme que o frontend está enviando o header `Authorization: Bearer <token>` - Um Admin deve atribuir perfil e area para o usuario.
### `GET /auth/config` retorna os provedores errados ### Microsoft OAuth falha
- Verifique `LDAP_ENABLED` e `MICROSOFT_ENABLED` no `.env` - Verifique `MICROSOFT_REDIRECT_URI` no Azure e no `.env`.
- Reinicie o servidor — variáveis de ambiente são lidas na inicialização - Confirme `MICROSOFT_CLIENT_ID` e `MICROSOFT_CLIENT_SECRET`.
- Refaca o fluxo se o state expirar.
--- ---
## O que ainda falta para produção ## Estado atual
- [x] Tabela `usuarios` no banco de dados - [x] Login LDAP/AD
- [x] Tabela `usuarios_provedores` para vincular provedores externos ao usuário interno - [x] Login Microsoft OAuth
- [x] `sub` do JWT usando ID interno do banco - [x] State OAuth assinado
- [ ] Guard NestJS para proteger rotas privadas (`@UseGuards(AuthGuard)`) - [x] JWT emitido pela aplicacao
- [ ] Roles e permissões validadas no backend - [x] JWT validado nas rotas privadas
- [ ] Auditoria de login - [x] Roles no backend
- [ ] Trocar token na query string por cookie HTTP-only (reduz exposição no browser) - [x] Bearer token no frontend
- [x] Token fora de query string no OAuth
--- - [x] WebSocket WhatsApp protegido por token
- [x] SQL de acesso isolado em repository
## Documentacao complementar - [x] DTO e ValidationPipe no login
- [x] Rate limit global
A sincronizacao de usuarios autenticados com o banco, os perfis de acesso, as areas operacionais e os endpoints administrativos estao documentados em: - [ ] Auditoria formal de login em tabela propria
- [ ] Cookie HttpOnly como alternativa futura ao sessionStorage
- [`access-control.md`](./access-control.md)