Segundo fator: TOTP e SafeID — API
Cobre as duas formas de segundo fator do Portal 2.0 — TOTP (código de 6 dígitos gerado por um
app autenticador) e SafeID (login delegado a um provedor externo, usado por instituições com
integração SafeWeb) — e o autoatendimento de TOTP (ativar, confirmar e desativar). As rotas de
conclusão de um desafio (totp/verify, safe-id/verify) são públicas e fazem parte do fluxo
de login; as rotas de gestão do TOTP (enrollment, enrollment/confirm,
DELETE) exigem uma sessão já autenticada.
Funcionamento
Concluir TOTP (POST /authentication/totp/verify): valida o token do desafio (emitido pelo
login) pelo hash, confere que não expirou e que o client_id/IP batem com os do desafio original,
verifica o código de 6 dígitos contra o segredo TOTP cifrado do usuário e, se correto, cria a
sessão com assurance: MFA. Código errado ou desafio esgotado contam como tentativa; ao atingir o
limite, o desafio é consumido (fica inutilizável) sem gerar um erro diferente do código errado.
Concluir SafeID (POST /authentication/safe-id/verify): confere que o desafio (emitido pelo
login) não expirou e que client_id/IP batem, troca o code recebido por um access_token junto
ao provedor SafeID externo (com timeout) e, em sucesso, cria a sessão com assurance: MFA.
Qualquer falha na comunicação com o provedor — indisponibilidade, timeout, resposta sem token — vira
um único erro de indisponibilidade, sem vazar detalhe do provedor.
Ativar TOTP (POST /authentication/totp/enrollment → POST /authentication/totp/enrollment/confirm):
com uma sessão autenticada, o usuário pede um novo segredo TOTP (ainda pendente, não habilita
nada), obtém o provisioning_uri para escanear no app autenticador e confirma com um código de 6
dígitos para habilitar de fato. Desativar TOTP (DELETE /authentication/totp) apaga o segredo
e desabilita, sem exigir reautenticação além do access token.
Endpoints
| Método | Rota | Descrição |
|---|---|---|
| POST | /v1/authentication/totp/verify | Conclui o login com o código TOTP (público) |
| POST | /v1/authentication/safe-id/verify | Conclui o login via SafeID (público) |
| POST | /v1/authentication/totp/enrollment | Inicia a ativação do TOTP (autenticado) |
| POST | /v1/authentication/totp/enrollment/confirm | Confirma e habilita o TOTP (autenticado) |
| DELETE | /v1/authentication/totp | Desabilita o TOTP (autenticado) |
Versão: v1
Swagger:
POST /authentication/totp/verifyPOST /authentication/safe-id/verifyPOST /authentication/totp/enrollmentPOST /authentication/totp/enrollment/confirmDELETE /authentication/totp
Rota (Dev): http://localhost:3000/v1/authentication/...
Lógica de decisão de totp/verify e safe-id/verify (conclusão do desafio de login):
Permissões
| Rota | Guards | Acesso |
|---|---|---|
POST /authentication/totp/verify | nenhum (rate limit apenas) | Público — precisa apenas do token do desafio |
POST /authentication/safe-id/verify | nenhum (rate limit apenas) | Público — precisa apenas do code/state do desafio |
POST /authentication/totp/enrollment | IdentityOAuthAccessTokenGuard | Usuário autenticado (sobre si mesmo) |
POST /authentication/totp/enrollment/confirm | IdentityOAuthAccessTokenGuard | Usuário autenticado |
DELETE /authentication/totp | IdentityOAuthAccessTokenGuard | Usuário autenticado |
Nenhuma dessas rotas usa IdentityOriginGuard (diferente do login). Sem access token válido nas
três rotas de gestão, o guard lança IdentityInvalidOAuthAccessTokenError (401,
IDENTITY_INVALID_OAUTH_ACCESS_TOKEN).
Headers
| Header | Obrigatório | Descrição |
|---|---|---|
Content-Type | Sim (rotas com corpo) | application/json |
Authorization | Sim (enrollment/confirm/DELETE) | Bearer <access_token> |
Path parameters
Nenhum.
Query parameters
Nenhum.
Body
Concluir TOTP — IdentityTotpLoginVerificationRequest:
json{ "token": "opaque-challenge-token", "code": "123456", "client_id": "web-portal" }
| Campo | Tipo | Obrigatório | Validação |
|---|---|---|---|
token | string | Sim | @Length(1, 512) — token do desafio recebido no login |
code | string | Sim | @Length(6, 6) — exatamente 6 dígitos |
client_id | string | Sim | @Length(1, 128) |
Concluir SafeID — IdentitySafeIdChallengeVerificationRequest:
json{ "code": "auth-code-do-safeid", "state": "b6b6...-uuid", "client_id": "web-portal" }
| Campo | Tipo | Obrigatório | Validação |
|---|---|---|---|
code | string | Sim | @Length(1, 2048) — código de autorização devolvido pelo SafeID |
state | string | Sim | @IsUUID — o mesmo state recebido no login |
client_id | string | Sim | @Length(1, 128) |
Confirmar ativação — IdentityTotpEnrollmentConfirmation:
json{ "code": "123456" }
totp/enrollment e DELETE /totp não recebem corpo (identificam o usuário pelo access token).
Response
200 — totp/verify e safe-id/verify (mesmo formato do login autenticado):
json{ "session_id": "b3f1...-uuid", "session_expires_at": "2026-10-24T12:00:00.000Z", "user_id": "8f2a...-uuid" }
200 — totp/enrollment (IdentityTotpEnrollmentResponse):
json{ "secret": "JBSWY3DPEHPK3PXP", "provisioning_uri": "otpauth://totp/Mobilemed:usuario@exemplo.com?secret=...&issuer=Mobilemed" }
200 — totp/enrollment/confirm: { "message": "TOTP enabled." }
200 — DELETE /totp: { "message": "TOTP disabled." }
Erros
| Rota | Classe de erro | errorCode | Status | Quando ocorre |
|---|---|---|---|---|
totp/verify | IdentityInvalidTotpChallengeError | IDENTITY_TOTP_CHALLENGE_INVALID | 401 | token inexistente/consumido/expirado, client_id/IP não batem, credencial sem TOTP, ou código errado — todos colapsam neste único erro |
safe-id/verify | (via invalidChallenge()) | IDENTITY_SAFE_ID_CHALLENGE_INVALID | 400 | desafio inexistente/expirado ou client_id/IP não batem |
safe-id/verify | — | IDENTITY_SAFE_ID_CODE_INVALID | 400 | code vazio |
safe-id/verify | IdentitySafeIdUnavailableError | IDENTITY_SAFE_ID_UNAVAILABLE | 503 | provedor SafeID indisponível, timeout, ou resposta sem access_token |
totp/enrollment | — | IDENTITY_TOTP_USER_UNAVAILABLE | 400 | usuário/credencial não encontrados |
totp/enrollment | — | IDENTITY_TOTP_ALREADY_ENABLED | 400 | TOTP já habilitado |
totp/enrollment/confirm | — | IDENTITY_TOTP_ENROLLMENT_NOT_STARTED | 400 | não há segredo pendente (não chamou enrollment antes) |
totp/enrollment/confirm | — | IDENTITY_TOTP_CODE_INVALID | 400 | código de confirmação errado — sem limite de tentativas ou bloqueio |
DELETE /totp | — | IDENTITY_TOTP_USER_UNAVAILABLE | 400 | usuário/credencial não encontrados |
| qualquer rota autenticada | IdentityInvalidOAuthAccessTokenError | IDENTITY_INVALID_OAUTH_ACCESS_TOKEN | 401 | access token ausente/inválido/expirado ou sessão inativa |
totp/verify, safe-id/verify | ThrottlerException | RATE_LIMIT_EXCEEDED | 429 | limite de 10/60s excedido |
Regras de negócio
| ID | Regra | Comportamento |
|---|---|---|
| RN-01 | client_id e IP do desafio devem bater com os da conclusão | tanto em TOTP quanto em SafeID; divergência → desafio tratado como inválido |
| RN-02 | TTL do desafio TOTP | 10 minutos (IDENTITY_TOTP_CHALLENGE_TTL_MINUTES) |
| RN-03 | Limite de tentativas do desafio TOTP | 5 (IDENTITY_TOTP_CHALLENGE_MAX_ATTEMPTS); ao atingir, o desafio é consumido (não dá para tentar de novo mesmo dentro do TTL) |
| RN-04 | TTL do desafio SafeID | 600s / 10 min (IDENTITY_SAFE_ID_LIFETIME_SECONDS) |
| RN-05 | Verificação TOTP usa janela de tolerância | algoritmo SHA1, 6 dígitos, passo de 30s, window: 1 (aceita o código do passo anterior/seguinte) |
| RN-06 | Sucesso cria sessão com assurance: MFA | tanto em TOTP quanto em SafeID |
| RN-07 | Iniciar enrollment de novo sobrescreve o segredo pendente | não há acumulação; o segredo anterior pendente é substituído |
| RN-08 | Confirmar enrollment não tem limite de tentativas | diferente do desafio de login, pode tentar quantas vezes quiser (sujeito só a rate limit global, se houver) |
| RN-09 | Desabilitar TOTP já desabilitado é no-op | DELETE /totp sem TOTP ativo retorna sucesso sem lançar erro |
| RN-10 | Desabilitar TOTP não exige reautenticação extra | basta um access token válido — não pede senha nem código TOTP atual |
Compliance
| Órgão / norma | Exigência | Como a rota atende |
|---|---|---|
| LGPD | minimização; segredo TOTP nunca em claro | segredo TOTP cifrado em repouso (AES-GCM); resposta de erro nunca revela qual validação falhou |
| HIPAA | autenticação multifator; trilha de eventos | eventos de auditoria auth.totp.challenge_issued, auth.totp.verified, auth.totp.enrollment_started, auth.totp.enabled, auth.totp.disabled, auth.safe_id.verified |
| ANVISA (indireto) | segundo fator validado e sob controle | limite de tentativas no desafio de login; TTL curto em ambos os desafios |
Variáveis de ambiente
| Variável | Uso | Default |
|---|---|---|
IDENTITY_TOTP_ISSUER | nome exibido no app autenticador (provisioning_uri) | Mobilemed |
IDENTITY_TOTP_CHALLENGE_TTL_MINUTES | TTL do desafio TOTP de login | 10 min (máx. 60) |
IDENTITY_TOTP_CHALLENGE_MAX_ATTEMPTS | tentativas máximas no desafio TOTP de login | 5 (máx. 10) |
IDENTITY_SAFE_ID_BASE_URL | URL base do provedor SafeID | https://pscsafeweb.safewebpss.com.br/... |
IDENTITY_SAFE_ID_CLIENT_ID / _CLIENT_SECRET | credenciais do client junto ao SafeID | vazio (sem isso, iniciar o desafio falha) |
IDENTITY_SAFE_ID_REDIRECT_URL | redirect_uri usada na troca com o SafeID | http://localhost:3000/v1/auth/safe-id/callback |
IDENTITY_SAFE_ID_LIFETIME_SECONDS | TTL do desafio SafeID | 600s (máx. 3600) |
IDENTITY_SAFE_ID_TIMEOUT_MS | timeout da chamada HTTP ao provedor | 10.000ms (máx. 60.000) |
IDENTITY_ENCRYPTION_KEY | cifra o segredo TOTP e o code_verifier do SafeID em repouso | obrigatória, sem default |
IDENTITY_SESSION_LIFETIME_DAYS | validade da sessão criada em sucesso | 30 dias |
Tempo médio de resposta
A confirmar — responsável: time de Identity; data: 24/09/2026. Sem medição publicada.
Requisitos não funcionais
| Requisito | Definição |
|---|---|
| Idempotência | Não nos desafios (single-use); DELETE /totp é idempotente em efeito (repetir não gera erro) |
| Rate limit | totp/verify/safe-id/verify: 10/60s (IP + identidade); rotas de gestão de TOTP não têm @Throttle próprio |
| Cache | Não |
| Auditoria | Sim, ver eventos listados em Compliance |
Divergências e lacunas confirmadas
- Não há teste automatizado dedicado (unitário ou e2e) cobrindo TOTP incorreto no desafio de
login, esgotamento de tentativas ou indisponibilidade do SafeID — a cobertura real hoje é zero
para esses cenários de borda (confirmado por busca nos diretórios
__test__do pacote). - O Swagger de
safe-id/verifydocumenta apenas400 BAD_REQUESTgenérico; oerrorCodereal devolvido ao cliente (IDENTITY_SAFE_ID_CHALLENGE_INVALID) não aparece no exemplo.
Relacionado
- 🖥️ Tela:
A confirmar — responsável: time de frontend; data: 24/09/2026.(não localizada neste levantamento, que cobriu apenas o backend) - 📂 Módulo: Authentication
- 🔑 Etapa anterior: Login