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

@ -29,8 +29,9 @@ Frontend
-> Backend cria/atualiza usuarios -> Backend cria/atualiza usuarios
-> 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
``` ```
@ -296,45 +297,56 @@ http://localhost:3001/admin/access/options
http://localhost:3001/admin/access/users 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.
```js O caminho recomendado e:
localStorage.setItem('authUser', JSON.stringify({
username: 'admin@sothis.com.br', 1. criar o usuario real no AD ou Microsoft;
name: 'Admin Demo', 2. fazer login pela tela;
email: 'admin@sothis.com.br', 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
sessionStorage.setItem('authUser', JSON.stringify({
username: 'admin@sothis.com.br',
name: 'Admin Demo',
email: 'admin@sothis.com.br',
perfis: ['Admin'], perfis: ['Admin'],
profiles: ['Admin'], profiles: ['Admin'],
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',
perfis: [], perfis: [],
profiles: [], profiles: [],
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';
``` ```
--- ---
## 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. - Registrar auditoria em `logs_auditoria`.
- Proteger `/admin/access/*` para `Admin`.
- 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.

609
Auth.md

@ -1,299 +1,310 @@
# 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 ```
``` ---
src/modules/auth/
├── auth.module.ts # Registro do módulo no NestJS ## Estrutura de arquivos
├── auth.controller.ts # Rotas HTTP
├── auth.service.ts # Fachada — delega para os providers ```text
├── auth.config.ts # Leitura de variáveis de ambiente src/modules/auth/
├── auth-token.service.ts # Emissão de JWT da aplicação |-- auth.module.ts
├── user-access.service.ts # Sincronização do usuário autenticado com o banco |-- auth.controller.ts
├── auth.types.ts # Interfaces TypeScript compartilhadas |-- auth.service.ts
└── providers/ |-- auth.config.ts
├── ldap-auth.provider.ts # Autenticação LDAP/AD |-- auth-token.service.ts
├── microsoft-oauth.provider.ts # Autenticação Microsoft OAuth |-- auth.types.ts
└── oauth-state.service.ts # Proteção CSRF para OAuth |-- user-access.service.ts
``` |-- decorators/
| |-- public.decorator.ts
--- | `-- roles.decorator.ts
|-- dto/
## Rotas disponíveis | `-- login.dto.ts
|-- guards/
| Método | Rota | Descrição | | |-- jwt-auth.guard.ts
|--------|---------------------------------|------------------------------------------------| | `-- roles.guard.ts
| GET | `/auth/config` | Retorna quais provedores estão habilitados | |-- providers/
| POST | `/auth/login` | Login com usuário e senha (LDAP/AD) | | |-- ldap-auth.provider.ts
| GET | `/auth/oauth/microsoft/start` | Inicia o fluxo OAuth com a Microsoft | | |-- microsoft-oauth.provider.ts
| GET | `/auth/oauth/microsoft/callback`| Callback que a Microsoft chama após o login | | `-- oauth-state.service.ts
`-- repositories/
--- `-- user-access.repository.ts
```
## Variáveis de ambiente
Responsabilidades principais:
```env
# Servidor - `AuthController`: expoe rotas publicas de login e OAuth.
PORT=3001 - `AuthService`: fachada que delega para LDAP ou Microsoft.
FRONTEND_URL=http://localhost:3000 - `AuthTokenService`: emite e valida JWT.
- `JwtAuthGuard`: exige Bearer token nas rotas privadas.
# JWT - `RolesGuard`: valida perfis com `@Roles()`.
JWT_SECRET=uma-chave-longa-e-aleatoria - `UserAccessService`: regra de sincronizacao do usuario autenticado.
JWT_EXPIRES_IN=8h - `UserAccessRepository`: instrucoes SQL do fluxo de acesso.
- `LoginDto`: validacao formal do payload de login.
# Provedores ativos (separados por vírgula)
AUTH_PROVIDERS=ldap ---
# LDAP / Active Directory ## Rotas publicas
LDAP_ENABLED=true
LDAP_URL=ldaps://servidor-ad:636 As rotas abaixo nao exigem JWT:
LDAP_DOMAIN=empresa.com.br
LDAP_USER_DN_TEMPLATE={{username}}@empresa.com.br | Metodo | Rota | Descricao |
LDAP_SEARCH_BASE=DC=empresa,DC=com |---|---|---|
LDAP_SEARCH_FILTER=(&(objectClass=user)(sAMAccountName={{username}})) | GET | `/health` | Health check da API |
LDAP_TIMEOUT_MS=5000 | GET | `/auth/config` | Informa provedores de login habilitados |
| POST | `/auth/login` | Login LDAP/AD |
# Microsoft Entra ID (desabilitado por padrão) | GET | `/auth/oauth/microsoft/start` | Inicia login Microsoft |
MICROSOFT_ENABLED=false | GET | `/auth/oauth/microsoft/callback` | Recebe callback Microsoft |
MICROSOFT_TENANT_ID=common
MICROSOFT_CLIENT_ID= Todas as demais rotas HTTP exigem JWT valido.
MICROSOFT_CLIENT_SECRET=
MICROSOFT_REDIRECT_URI=http://localhost:3001/auth/oauth/microsoft/callback ---
MICROSOFT_SUCCESS_REDIRECT_URL=http://localhost:3000/login
``` ## Rotas privadas e perfis
> `JWT_SECRET` deve ser uma string longa e aleatória. Em produção, nunca use o valor padrão do `.env.development`. O `JwtAuthGuard` foi registrado como guard global. Isso significa que toda rota e privada por padrao, exceto as marcadas com `@Public()`.
--- Exemplo:
## Fluxo LDAP / Active Directory ```typescript
@Public()
``` @Get('config')
Frontend getConfig() {}
→ POST /auth/login { username, password } ```
→ AuthController
→ AuthService.loginWithLdap() Para autorizacao por perfil, o backend usa `@Roles()`:
→ LdapAuthProvider.authenticate()
→ Conecta no servidor AD (LDAP_URL) ```typescript
→ Faz bind com o usuário e senha @Roles('Admin')
→ Se o bind falhar: UnauthorizedException @Get('users')
→ Busca dados do usuário no diretório (se LDAP_SEARCH_BASE configurado) listUsers() {}
→ Monta objeto AuthenticatedUser ```
→ UserAccessService.syncAuthenticatedUser()
→ Cria/atualiza usuarios e usuarios_provedores Regras atuais:
→ Carrega perfis, areas e area principal
→ AuthTokenService.issueToken() - `Admin`: acessa administracao, usuarios, areas, base de conhecimento e configuracoes sensiveis.
→ Gera JWT assinado com JWT_SECRET - `Supervisor`: acessa visoes operacionais liberadas para supervisao.
→ Retorna { token, user } para o frontend - `Agente`: acessa fluxos autenticados sem permissao administrativa.
```
---
O AD apenas valida a identidade. O JWT emitido é da aplicação, não do AD.
## Fluxo LDAP / Active Directory
---
```text
## Fluxo Microsoft OAuth Frontend
-> POST /auth/login { username, password }
``` -> LoginDto valida payload
1. Frontend redireciona para GET /auth/oauth/microsoft/start -> AuthController
→ Backend gera um state assinado (proteção CSRF) -> AuthService.loginWithLdap()
→ Backend redireciona para login.microsoftonline.com -> LdapAuthProvider.authenticate()
-> valida se LDAP esta habilitado
2. Usuário autentica na Microsoft -> conecta no servidor LDAP
-> faz bind com usuario e senha
3. Microsoft chama GET /auth/oauth/microsoft/callback?code=...&state=... -> busca dados do usuario no diretorio, se configurado
→ Backend valida o state (assinatura + expiração) -> monta AuthenticatedUser
→ Backend troca o code por access_token (chamada server-to-server) -> UserAccessService.syncAuthenticatedUser()
→ Backend consulta Microsoft Graph /me para obter dados do usuário -> UserAccessRepository executa SQL
→ UserAccessService.syncAuthenticatedUser() sincroniza o usuário no banco -> cria/atualiza usuarios
→ AuthTokenService.issueToken() gera JWT próprio -> cria/atualiza usuarios_provedores
→ Backend redireciona para MICROSOFT_SUCCESS_REDIRECT_URL?token=... -> carrega perfis e areas
-> AuthTokenService.issueToken()
4. Frontend salva o token e navega para /home -> retorna { token, user }
``` ```
--- 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.
## Proteção CSRF com OAuth State ---
O `OAuthStateService` protege o fluxo OAuth contra ataques de CSRF. ## Fluxo Microsoft OAuth
**Como funciona:** ```text
Frontend
1. No início do fluxo, o backend cria um state: -> GET /auth/oauth/microsoft/start
- Gera um nonce aleatório + timestamp Backend
- Converte para base64url -> gera state assinado com HMAC-SHA256 usando JWT_SECRET
- Assina com HMAC-SHA256 usando o `JWT_SECRET` -> redireciona para Microsoft
- Formato final: `payload.assinatura` Usuario
-> autentica na Microsoft
2. No callback, o backend verifica: Microsoft
- O state tem os dois pedaços (`payload.assinatura`) -> chama /auth/oauth/microsoft/callback?code=...&state=...
- A assinatura é válida (recalcula e compara com `timingSafeEqual`) Backend
- O state não expirou (padrão: 10 minutos, configurável via `MICROSOFT_STATE_MAX_AGE_MS`) -> valida state
-> troca code por access_token
Se qualquer verificação falhar, o callback é rejeitado com `400 Bad Request`. -> consulta Microsoft Graph /me
-> sincroniza usuario no banco
--- -> emite JWT da aplicacao
-> redireciona para o frontend com payload no fragmento da URL
## JWT da aplicação Frontend
-> le #auth=...
Após qualquer autenticação bem-sucedida, o `AuthTokenService` emite um JWT com o seguinte payload: -> salva sessao em sessionStorage
-> limpa a URL
```json ```
{
"sub": "4", 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.
"name": "Nome Completo",
"email": "usuario@empresa.com", ---
"provider": "ldap",
"username": "usuario", ## JWT da aplicacao
"perfis": ["Admin"],
"profiles": ["Admin"], Payload principal:
"areas": ["Suporte"],
"areaPrincipal": "Suporte", ```json
"accessStatus": "assigned" {
} "sub": "4",
``` "name": "Nome Completo",
"email": "usuario@empresa.com",
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`. "provider": "ldap",
"username": "usuario",
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. "perfis": ["Admin"],
"profiles": ["Admin"],
--- "areas": ["Suporte"],
"areaPrincipal": "Suporte",
## Sincronização de usuário e acesso "accessStatus": "assigned"
}
Após o provedor autenticar o usuário, o `UserAccessService`: ```
1. faz upsert em `usuarios` usando email ou fallback `provider:username`; O `sub` usa o ID interno da tabela `usuarios`.
2. faz upsert em `usuarios_provedores`;
3. consulta perfis em `usuarios_perfis` + `perfis_acesso`; O token e:
4. consulta especialidades em `usuarios_areas` + `areas`;
5. retorna o usuário enriquecido com: - assinado com `JWT_SECRET`;
- `databaseId`; - emitido com expiracao definida por `JWT_EXPIRES_IN`;
- `perfis` / `profiles`; - validado em todas as rotas privadas;
- `areas`; - enviado pelo frontend como Bearer token.
- `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. ## Sessao no frontend
--- O frontend armazena `authToken` e `authUser` em `sessionStorage`, nao em `localStorage`.
## Como adicionar um novo provedor Motivo:
1. Crie o arquivo em `src/modules/auth/providers/novo-provedor.provider.ts`: - reduz persistencia indevida apos fechar o navegador;
- evita reaproveitamento de sessoes antigas salvas localmente;
```typescript - continua simples para o modelo atual do produto.
import { Injectable } from '@nestjs/common';
import { AuthConfigService } from '../auth.config'; Observacao: uma alternativa ainda mais forte para producao seria cookie `HttpOnly` + `Secure` + `SameSite`, mas exigiria ajuste maior no fluxo do frontend e no CORS.
import { AuthTokenService } from '../auth-token.service';
import { AuthResult } from '../auth.types'; ---
@Injectable() ## WebSocket WhatsApp
export class NovoProvedorProvider {
constructor( O namespace Socket.IO `/whatsapp` tambem exige JWT no handshake.
private readonly authConfig: AuthConfigService,
private readonly authToken: AuthTokenService, O frontend envia:
) {}
```javascript
async authenticate(/* dados necessários */): Promise<AuthResult> { io(WHATSAPP_SOCKET_URL, {
// 1. Valide as credenciais no provedor externo auth: {
// 2. Monte o objeto AuthenticatedUser token: getAuthToken()
// 3. Emita o token com this.authToken.issueToken(user) }
// 4. Retorne { token, user } });
} ```
}
``` Sem token valido, o socket nao conecta e nao recebe QR Code, status ou mensagens.
2. Registre o provider em `auth.module.ts`: ---
```typescript ## Rate limit
providers: [
AuthConfigService, O backend usa `@nestjs/throttler` com limite global configuravel:
AuthService,
AuthTokenService, ```env
LdapAuthProvider, RATE_LIMIT_TTL_MS=60000
MicrosoftOAuthProvider, RATE_LIMIT_MAX=300
OAuthStateService, ```
NovoProvedorProvider, // adicione aqui
], Padrao atual: 300 requisicoes por IP a cada 60 segundos.
```
---
3. Injete no `AuthService` e exponha o método necessário:
## Variaveis de ambiente
```typescript
constructor( ```env
private readonly authConfig: AuthConfigService, FRONTEND_URL=http://localhost:3000
private readonly ldapAuthProvider: LdapAuthProvider, JWT_SECRET=uma-chave-longa-e-aleatoria
private readonly microsoftOAuthProvider: MicrosoftOAuthProvider, JWT_EXPIRES_IN=8h
private readonly novoProvedorProvider: NovoProvedorProvider, // injete aqui
) {} RATE_LIMIT_TTL_MS=60000
RATE_LIMIT_MAX=300
loginComNovoProvedor(dados: any) {
return this.novoProvedorProvider.authenticate(dados); AUTH_PROVIDERS=ldap,microsoft
}
``` LDAP_ENABLED=true
LDAP_URL=ldaps://servidor-ad:636
4. Adicione a rota no `AuthController`. LDAP_DOMAIN=empresa.com.br
LDAP_USER_DN_TEMPLATE={{username}}@empresa.com.br
5. Se o provedor precisar de configuração, adicione as variáveis no `AuthConfigService` e no `.env`. LDAP_SEARCH_BASE=DC=empresa,DC=com
LDAP_SEARCH_FILTER=(&(objectClass=user)(sAMAccountName={{username}}))
--- LDAP_TIMEOUT_MS=5000
## Diagnóstico de problemas MICROSOFT_ENABLED=false
MICROSOFT_TENANT_ID=common
### Login LDAP falha com `UnauthorizedException` MICROSOFT_CLIENT_ID=
MICROSOFT_CLIENT_SECRET=
- Verifique se `LDAP_URL` está acessível a partir do servidor backend MICROSOFT_REDIRECT_URI=http://localhost:3001/auth/oauth/microsoft/callback
- Confirme que `LDAP_DOMAIN` ou `LDAP_USER_DN_TEMPLATE` está correto MICROSOFT_SUCCESS_REDIRECT_URL=http://localhost:3000/login
- 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 `JWT_SECRET` deve ser forte e exclusivo por ambiente.
### 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 ## Diagnostico
- 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 ### Login LDAP falha
### Token inválido no frontend - Verifique `LDAP_URL`, `LDAP_DOMAIN` e `LDAP_USER_DN_TEMPLATE`.
- Confirme conectividade do backend com o AD.
- Verifique se `JWT_SECRET` não mudou entre deploys — isso invalida todos os tokens emitidos anteriormente - Verifique se `LDAP_SEARCH_BASE` e `LDAP_SEARCH_FILTER` batem com o diretorio.
- Confirme que o frontend está enviando o header `Authorization: Bearer <token>` - Consulte logs do backend; o provider registra o motivo interno sem expor para o usuario.
### `GET /auth/config` retorna os provedores errados ### Token invalido ou expirado
- Verifique `LDAP_ENABLED` e `MICROSOFT_ENABLED` no `.env` - Verifique se o frontend esta enviando `Authorization: Bearer <token>`.
- Reinicie o servidor — variáveis de ambiente são lidas na inicialização - Verifique se `JWT_SECRET` nao mudou depois da emissao do token.
- Faca login novamente para renovar a sessao.
---
### Usuario autenticado cai como `unassigned`
## O que ainda falta para produção
- O login funcionou, mas faltam perfil/area no banco.
- [x] Tabela `usuarios` no banco de dados - Um Admin deve atribuir perfil e area para o usuario.
- [x] Tabela `usuarios_provedores` para vincular provedores externos ao usuário interno
- [x] `sub` do JWT usando ID interno do banco ### Microsoft OAuth falha
- [ ] Guard NestJS para proteger rotas privadas (`@UseGuards(AuthGuard)`)
- [ ] Roles e permissões validadas no backend - Verifique `MICROSOFT_REDIRECT_URI` no Azure e no `.env`.
- [ ] Auditoria de login - Confirme `MICROSOFT_CLIENT_ID` e `MICROSOFT_CLIENT_SECRET`.
- [ ] Trocar token na query string por cookie HTTP-only (reduz exposição no browser) - Refaca o fluxo se o state expirar.
--- ---
## Documentacao complementar ## Estado atual
A sincronizacao de usuarios autenticados com o banco, os perfis de acesso, as areas operacionais e os endpoints administrativos estao documentados em: - [x] Login LDAP/AD
- [x] Login Microsoft OAuth
- [`access-control.md`](./access-control.md) - [x] State OAuth assinado
- [x] JWT emitido pela aplicacao
- [x] JWT validado nas rotas privadas
- [x] Roles no backend
- [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
- [x] DTO e ValidationPipe no login
- [x] Rate limit global
- [ ] Auditoria formal de login em tabela propria
- [ ] Cookie HttpOnly como alternativa futura ao sessionStorage