Login com passkey
Entra com biometria ou chave de segurança (WebAuthn), sem digitar senha, em duas chamadas.
Uma passkey é uma credencial guardada no aparelho do cliente (Face ID, impressão digital, Windows Hello ou chave física) que substitui a senha. O padrão técnico é o WebAuthn: o servidor manda um desafio, o navegador pede a biometria e devolve uma prova assinada. A plataforma suporta passkeys de descoberta automática: o cliente não digita e-mail, o próprio aparelho escolhe a credencial.
O fluxo completo tem três partes, em três páginas:
- Cadastrar a passkey (cliente já logado, uma vez por aparelho).
- Entrar com a passkey (esta página).
- Listar e remover passkeys.
Há ainda a confirmação por biometria (step-up), para ações sensíveis.
/api/v1/auth/webauthn/login/beginPúblicaIniciar o login com passkey
Gera o desafio. Não recebe corpo. O options.publicKey da resposta vai direto para navigator.credentials.get.
Exemplo de requisição
curl -X POST "$BASE_URL/api/v1/auth/webauthn/login/begin"Resposta
{
"ceremony_id": "647bb41f-0aec-46ac-a2aa-9396f65ea311",
"options": {
"publicKey": {
"challenge": "qrTUyCAThPPHs-5tfQvPMVj_nlVvoKMx9mbcVZK11v4",
"timeout": 300000,
"rpId": "minhaloja.io.venddor.com.br"
}
}
}Campos da resposta
ceremony_iduuidfinish.optionsobjectpublicKey traz challenge (desafio em base64url), timeout (milissegundos) e rpId (host da loja, sem porta)./api/v1/auth/webauthn/login/finishPúblicaConcluir o login com passkey
Verifica a prova do aparelho e abre a sessão.
Corpo da requisição
ceremony_iduuidobrigatórioceremony_id recebido no begin.credentialobjectobrigatórionavigator.credentials.get serializada em JSON (id, rawId, type, response).accept_pending_sessionbooleanpadrão: falseaccount_pending_approval. Com true, ele recebe uma sessão restrita às rotas de cadastro.Exemplo de requisição
curl -X POST "$BASE_URL/api/v1/auth/webauthn/login/finish" \
-H "Content-Type: application/json" \
-d '{
"ceremony_id": "647bb41f-0aec-46ac-a2aa-9396f65ea311",
"credential": {
"id": "AQIDBAUG...",
"rawId": "AQIDBAUG...",
"type": "public-key",
"response": {
"clientDataJSON": "eyJ0eXBl...",
"authenticatorData": "SZYN5Y...",
"signature": "MEUCIQ...",
"userHandle": "M2YwYzJh..."
}
}
}'No navegador, o conjunto das duas chamadas fica assim (navegadores recentes oferecem parseRequestOptionsFromJSON e toJSON, que evitam a conversão manual de base64url):
const begin = await (await fetch("/api/v1/auth/webauthn/login/begin", { method: "POST" })).json();
const publicKey = PublicKeyCredential.parseRequestOptionsFromJSON(begin.options.publicKey);
const cred = await navigator.credentials.get({ publicKey });
const res = await fetch("/api/v1/auth/webauthn/login/finish", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ceremony_id: begin.ceremony_id, credential: cred.toJSON() }),
});
const tokens = await res.json();Resposta
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_token": "rt_9f2c1d7e4b...",
"expires_at": "2026-10-11T21:30:00Z"
}Campos da resposta
access_tokenstringrefresh_tokenstringPOST /api/v1/auth/refresh.expires_atstringaccess_token (RFC 3339, UTC).account_statusstringpending_approvalErros
| Status | Quando | Exemplo de corpo |
|---|---|---|
| 400 | Corpo do finish que não é JSON. | {"error":"invalid body","detail":"..."} |
| 401 | Prova inválida, cerimônia expirada ou já usada, ou passkey desconhecida. | {"error":"login failed","detail":"..."} |
| 403 | Cadastro aguardando aprovação e accept_pending_session ausente. | {"error":"account_pending_approval"} |
| 403 | Conta desativada. | {"error":"account_disabled"} |
| 429 | Limite de requisições da família /auth/. | {"error":"rate limit exceeded","detail":"too many requests, please try again later"} |
| 500 | Falha ao gerar o desafio no begin. | {"error":"failed to begin login","detail":"..."} |