Skip to main content

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étodoRotaDescrição
POST/v1/authentication/totp/verifyConclui o login com o código TOTP (público)
POST/v1/authentication/safe-id/verifyConclui o login via SafeID (público)
POST/v1/authentication/totp/enrollmentInicia a ativação do TOTP (autenticado)
POST/v1/authentication/totp/enrollment/confirmConfirma e habilita o TOTP (autenticado)
DELETE/v1/authentication/totpDesabilita o TOTP (autenticado)

Versão: v1

Swagger:

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​

RotaGuardsAcesso
POST /authentication/totp/verifynenhum (rate limit apenas)Público — precisa apenas do token do desafio
POST /authentication/safe-id/verifynenhum (rate limit apenas)Público — precisa apenas do code/state do desafio
POST /authentication/totp/enrollmentIdentityOAuthAccessTokenGuardUsuário autenticado (sobre si mesmo)
POST /authentication/totp/enrollment/confirmIdentityOAuthAccessTokenGuardUsuário autenticado
DELETE /authentication/totpIdentityOAuthAccessTokenGuardUsuá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​

HeaderObrigatórioDescrição
Content-TypeSim (rotas com corpo)application/json
AuthorizationSim (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" }
CampoTipoObrigatórioValidação
tokenstringSim@Length(1, 512) — token do desafio recebido no login
codestringSim@Length(6, 6) — exatamente 6 dígitos
client_idstringSim@Length(1, 128)

Concluir SafeID — IdentitySafeIdChallengeVerificationRequest:

json
{ "code": "auth-code-do-safeid", "state": "b6b6...-uuid", "client_id": "web-portal" }
CampoTipoObrigatórioValidação
codestringSim@Length(1, 2048) — código de autorização devolvido pelo SafeID
statestringSim@IsUUID — o mesmo state recebido no login
client_idstringSim@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​

RotaClasse de erroerrorCodeStatusQuando ocorre
totp/verifyIdentityInvalidTotpChallengeErrorIDENTITY_TOTP_CHALLENGE_INVALID401token 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_INVALID400desafio inexistente/expirado ou client_id/IP não batem
safe-id/verify—IDENTITY_SAFE_ID_CODE_INVALID400code vazio
safe-id/verifyIdentitySafeIdUnavailableErrorIDENTITY_SAFE_ID_UNAVAILABLE503provedor SafeID indisponível, timeout, ou resposta sem access_token
totp/enrollment—IDENTITY_TOTP_USER_UNAVAILABLE400usuário/credencial não encontrados
totp/enrollment—IDENTITY_TOTP_ALREADY_ENABLED400TOTP já habilitado
totp/enrollment/confirm—IDENTITY_TOTP_ENROLLMENT_NOT_STARTED400não há segredo pendente (não chamou enrollment antes)
totp/enrollment/confirm—IDENTITY_TOTP_CODE_INVALID400código de confirmação errado — sem limite de tentativas ou bloqueio
DELETE /totp—IDENTITY_TOTP_USER_UNAVAILABLE400usuário/credencial não encontrados
qualquer rota autenticadaIdentityInvalidOAuthAccessTokenErrorIDENTITY_INVALID_OAUTH_ACCESS_TOKEN401access token ausente/inválido/expirado ou sessão inativa
totp/verify, safe-id/verifyThrottlerExceptionRATE_LIMIT_EXCEEDED429limite de 10/60s excedido

Regras de negócio​

IDRegraComportamento
RN-01client_id e IP do desafio devem bater com os da conclusãotanto em TOTP quanto em SafeID; divergência → desafio tratado como inválido
RN-02TTL do desafio TOTP10 minutos (IDENTITY_TOTP_CHALLENGE_TTL_MINUTES)
RN-03Limite de tentativas do desafio TOTP5 (IDENTITY_TOTP_CHALLENGE_MAX_ATTEMPTS); ao atingir, o desafio é consumido (não dá para tentar de novo mesmo dentro do TTL)
RN-04TTL do desafio SafeID600s / 10 min (IDENTITY_SAFE_ID_LIFETIME_SECONDS)
RN-05Verificação TOTP usa janela de tolerânciaalgoritmo SHA1, 6 dígitos, passo de 30s, window: 1 (aceita o código do passo anterior/seguinte)
RN-06Sucesso cria sessão com assurance: MFAtanto em TOTP quanto em SafeID
RN-07Iniciar enrollment de novo sobrescreve o segredo pendentenão há acumulação; o segredo anterior pendente é substituído
RN-08Confirmar enrollment não tem limite de tentativasdiferente do desafio de login, pode tentar quantas vezes quiser (sujeito só a rate limit global, se houver)
RN-09Desabilitar TOTP já desabilitado é no-opDELETE /totp sem TOTP ativo retorna sucesso sem lançar erro
RN-10Desabilitar TOTP não exige reautenticação extrabasta um access token válido — não pede senha nem código TOTP atual

Compliance​

Órgão / normaExigênciaComo a rota atende
LGPDminimização; segredo TOTP nunca em clarosegredo TOTP cifrado em repouso (AES-GCM); resposta de erro nunca revela qual validação falhou
HIPAAautenticação multifator; trilha de eventoseventos 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 controlelimite de tentativas no desafio de login; TTL curto em ambos os desafios

Variáveis de ambiente​

VariávelUsoDefault
IDENTITY_TOTP_ISSUERnome exibido no app autenticador (provisioning_uri)Mobilemed
IDENTITY_TOTP_CHALLENGE_TTL_MINUTESTTL do desafio TOTP de login10 min (máx. 60)
IDENTITY_TOTP_CHALLENGE_MAX_ATTEMPTStentativas máximas no desafio TOTP de login5 (máx. 10)
IDENTITY_SAFE_ID_BASE_URLURL base do provedor SafeIDhttps://pscsafeweb.safewebpss.com.br/...
IDENTITY_SAFE_ID_CLIENT_ID / _CLIENT_SECRETcredenciais do client junto ao SafeIDvazio (sem isso, iniciar o desafio falha)
IDENTITY_SAFE_ID_REDIRECT_URLredirect_uri usada na troca com o SafeIDhttp://localhost:3000/v1/auth/safe-id/callback
IDENTITY_SAFE_ID_LIFETIME_SECONDSTTL do desafio SafeID600s (máx. 3600)
IDENTITY_SAFE_ID_TIMEOUT_MStimeout da chamada HTTP ao provedor10.000ms (máx. 60.000)
IDENTITY_ENCRYPTION_KEYcifra o segredo TOTP e o code_verifier do SafeID em repousoobrigatória, sem default
IDENTITY_SESSION_LIFETIME_DAYSvalidade da sessão criada em sucesso30 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​

RequisitoDefinição
IdempotênciaNão nos desafios (single-use); DELETE /totp é idempotente em efeito (repetir não gera erro)
Rate limittotp/verify/safe-id/verify: 10/60s (IP + identidade); rotas de gestão de TOTP não têm @Throttle próprio
CacheNão
AuditoriaSim, 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/verify documenta apenas 400 BAD_REQUEST genérico; o errorCode real 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