Axuz Developers

Login com Axuz

Implemente "Entrar com Axuz" no seu app com OAuth 2.1 e OpenID Connect, PKCE S256, refresh com rotação e validação do id_token, com exemplo Node completo.

“Login com Axuz” permite que o seu app identifique pessoas pela conta Axuz sem tocar em senha, MFA ou passkey: a Axuz autentica, a pessoa consente e o seu app recebe um id_token assinado e, se pedir, um access_token para chamar APIs. O protocolo é Authorization Code com PKCE S256 (OAuth 2.1) mais OpenID Connect.

Decisões que valem para qualquer app

  • Só existem os grants authorization_code e refresh_token. Não há implicit, senha nem client_credentials.
  • PKCE é obrigatório e só aceita S256, inclusive para client confidential. plain é recusado.
  • state é obrigatório (de 8 a 512 caracteres).
  • A redirect_uri precisa ser idêntica a uma das registradas: sem curinga, sem prefixo, sem normalização.
  • O sub é o ID interno imutável do usuário (um UUID). Nunca é e-mail nem telefone: a chave de conta no seu app é o par iss + sub.
  • Só uma sessão plena Axuz autoriza (senha, e-mail e TOTP ou passkey). Quem tem sessão restrita é mandado cadastrar os fatores.
  • O code vale 60 segundos e é de uso único; reusá-lo revoga os tokens que ele já gerou.

1. Registrar o client

Cada app tem um client_id. O tipo public (SPA, app nativo) não tem segredo e se prova só por PKCE; o confidential (servidor) recebe também um client_secret.

POST/oauth/clients

O cadastro exige sessão plena e uma Origin confiável (proteção CSRF), por isso é feito pela sessão do navegador da conta, não por token pessoal. Ainda não há tela dedicada para a gestão de clients.

curl

curl -sS -X POST https://auth-api.axuz.net/oauth/clients \
  --cookie "access_token=$AXUZ_SESSION_JWT" \
  -H "Origin: https://auth.axuz.net" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Meu App",
    "client_type": "confidential",
    "redirect_uris": ["http://localhost:3000/callback", "https://app.example.test/callback"],
    "scopes": ["openid", "profile", "email", "offline_access"]
  }'

A resposta 201 traz client_id, client_type, redirect_uris, scopes, verified e, só para confidential, o client_secret, mostrado uma única vez (no banco fica só o hash). Regras do cadastro:

CampoRegra
namede 1 a 80 caracteres
redirect_urisde 1 a 10; https, ou http só em loopback (localhost, 127.0.0.1, [::1]), ou esquema próprio em client public/nativo; nunca fragmento, curinga, javascript: ou data:
scopesdo catálogo abaixo; o app só poderá pedir um subconjunto deles
team_idopcional; exige ser owner ou admin da equipe

Cada pessoa pode ter até 20 clients ativos (409 ao passar). Escopos sensíveis (forms:write, tasks:write, files:write, webhooks:manage, mail:send, me:write, me:profile) exigem client verified, aprovado por um operador da Axuz; no auto-atendimento eles são recusados. Também existem GET, PATCH e DELETE /oauth/clients/{client_id} e POST /oauth/clients/{client_id}/rotate-secret (o segredo antigo morre na hora). Excluir o client revoga todos os grants dele.

Escopos

EscopoO que entrega
openidliga o modo OIDC: id_token e acesso ao /oauth/userinfo (sub)
profilename
emailemail e email_verified
offline_accessemite refresh token
forms:*, tasks:*, files:*, webhooks:manage, mail:send, me:*acesso de API, espelhando o catálogo público GET /v1/scopes

2. O fluxo, passo a passo

  1. Gere o segredo da transação. Crie code_verifier (32 bytes aleatórios em base64url = 43 caracteres), code_challenge = BASE64URL(SHA256(code_verifier)), um state e um nonce, todos por CSPRNG. Guarde code_verifier, nonce e state ligados ao navegador (cookie HttpOnly mais armazenamento no servidor).
  2. Redirecione a pessoa para GET /oauth/authorize do provedor. Se ela não tem sessão, o provedor a leva ao login; se não consentiu ainda, à tela de consentimento; senão, volta direto.
  3. Receba o callback na sua redirect_uri com code, state e iss. Confira que o state bate com o do navegador e que iss é o issuer esperado (RFC 9207). Em caso de recusa vem error em vez de code.
  4. Troque o code em POST /oauth/token com o code_verifier.
  5. Valide o id_token e só então abra a sessão do seu app.
  6. Opcional: chame /oauth/userinfo com o access_token.

Passo 2: pedido de autorização

GET https://auth-api.axuz.net/oauth/authorize
  ?response_type=code
  &client_id=CLIENT_ID
  &redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fcallback
  &scope=openid%20profile%20email
  &state=ESTADO_ALEATORIO
  &nonce=NONCE_ALEATORIO
  &code_challenge=CHALLENGE_43_CHARS
  &code_challenge_method=S256
ParâmetroObrigatórioRegra
response_typesimsó code
client_id, redirect_urisimerro aqui não redireciona: responde 400 JSON (evita open redirect)
scopesimseparado por espaço; inclua openid para OIDC; cada escopo precisa estar no client
statesimde 8 a 512 caracteres
code_challengesim43 caracteres (SHA-256 em base64url)
code_challenge_methodsimsó S256
noncerecomendadoaté 512 caracteres; volta dentro do id_token
promptnãonone (sem tela: devolve login_required ou consent_required por redirect) ou consent (força a tela)

Os demais erros voltam por redirect à sua redirect_uri com error, error_description, state e iss.

Passo 4: troca do code

POST/oauth/token

O corpo é application/x-www-form-urlencoded. Client confidential se autentica com client_secret_basic (header Authorization: Basic) ou client_secret_post (campos no corpo); client public envia só o client_id.

curl

curl -sS -X POST https://auth-api.axuz.net/oauth/token \
  -u "$AXUZ_CLIENT_ID:$AXUZ_CLIENT_SECRET" \
  -d grant_type=authorization_code \
  -d "code=$CODE" \
  --data-urlencode "redirect_uri=http://localhost:3000/callback" \
  -d "code_verifier=$CODE_VERIFIER"

Node

const res = await fetch('https://auth-api.axuz.net/oauth/token', {
  method: 'POST',
  headers: { 'content-type': 'application/x-www-form-urlencoded' },
  body: new URLSearchParams({
    grant_type: 'authorization_code',
    code,
    redirect_uri: 'http://localhost:3000/callback',
    client_id: process.env.AXUZ_CLIENT_ID,
    client_secret: process.env.AXUZ_CLIENT_SECRET, // omita em client public
    code_verifier,
  }),
});
const tokens = await res.json();

A resposta 200 (com Cache-Control: no-store) tem access_token, token_type: "Bearer", expires_in (600), scope, id_token (com openid) e refresh_token (só com offline_access). Qualquer falha na troca (código expirado, já usado, redirect_uri diferente, verifier errado) responde a mesma mensagem invalid_grant, de propósito.

3. Validar o id_token

O id_token é um JWT de três partes assinado com EdDSA (Ed25519). Antes de confiar nele:

  1. Leia o header e exija alg = EdDSA. Recuse qualquer outro, inclusive none.
  2. Busque a chave pelo kid no JWKS (jwks_uri do discovery, /.well-known/jwks.json) e verifique a assinatura. Se o kid for novo, releia o JWKS uma vez: as chaves rotacionam.
  3. Confira iss igual ao issuer, aud igual ao seu client_id, exp no futuro (tolere pouca diferença de relógio) e nonce igual ao que você guardou.
  4. Use iss + sub como identidade.
ClaimQuando vem
iss, sub, aud, azp, exp, iatsempre (validade de 10 minutos)
noncese você enviou no pedido
nameescopo profile
email, email_verifiedescopo email

O access_token é um JWT typ: at+jwt (RFC 9068) de 10 minutos para as APIs Axuz (aud: axuz-api); não o confunda com o id_token nem o use como prova de login. Ele pode ser revogado a qualquer momento, por isso APIs que o aceitam o verificam no servidor.

4. Userinfo

GET/oauth/userinfo
curl -sS https://auth-api.axuz.net/oauth/userinfo -H "Authorization: Bearer $ACCESS_TOKEN"

Devolve {"sub":"...","name":"...","email":"...","email_verified":true} conforme os escopos. Sem o escopo openid a resposta é 403 insufficient_scope; token inválido, 401 invalid_token com WWW-Authenticate. Também aceita POST. Compare sub com o do id_token.

5. Refresh com rotação

Com offline_access você recebe um refresh token opaco. Renove em POST /oauth/token com grant_type=refresh_token e refresh_token; pode enviar scope com um subconjunto do concedido (nunca amplia, senão invalid_scope).

  • Cada refresh é de uso único: a resposta traz um novo, e você deve descartar o antigo.
  • Reusar um refresh já trocado é tratado como vazamento e revoga o grant inteiro (todos os tokens daquela autorização).
  • Validade deslizante de 30 dias, com teto absoluto de 90 dias desde o consentimento; depois disso a pessoa precisa consentir de novo.

6. Revogar e introspectar

POST/oauth/revoke

POST /oauth/revoke (RFC 7009) recebe token (access ou refresh) e a autenticação do client, e revoga o grant inteiro do token. Token desconhecido, expirado ou de outro client também responde 200, para não revelar existência. A pessoa pode retirar o consentimento em DELETE /oauth/consents/{client_id}, que revoga os grants dela naquele app. POST /oauth/introspect (RFC 7662) existe só para client confidential e só enxerga tokens do próprio client.

7. Discovery

GET /.well-known/openid-configuration lista issuer, authorization_endpoint, token_endpoint, userinfo_endpoint, jwks_uri, revocation_endpoint, introspection_endpoint, scopes_supported, code_challenge_methods_supported: ["S256"], id_token_signing_alg_values_supported: ["EdDSA"], token_endpoint_auth_methods_supported (client_secret_basic, client_secret_post, none) e authorization_response_iss_parameter_supported: true. Bibliotecas OIDC configuram tudo a partir dele; o issuer do documento deve ser igual ao que você configurou.

Erros OAuth

Os erros do protocolo seguem a RFC 6749: JSON {"error":"...","error_description":"..."}.

errorHTTPCausa comum
invalid_request400parâmetro ausente, redirect_uri não registrada, state curto, PKCE ausente
invalid_client401client_id desconhecido ou client_secret errado
invalid_grant400code ou refresh inválido, expirado ou reusado; verifier errado
unsupported_grant_type400qualquer grant além de authorization_code e refresh_token
invalid_scope400escopo desconhecido, fora do client ou maior que o concedido
access_deniedredirecta pessoa negou o consentimento
login_required, consent_requiredredirectprompt=none sem sessão ou sem consentimento
enrollment_required403sessão restrita: cadastre os fatores
invalid_token, insufficient_scope401, 403/oauth/userinfo com token ruim ou sem openid
rate_limited429falhas demais (client ou IP); respeite Retry-After

Exemplo Node completo

O repositório da documentação traz um servidor mínimo e executável em examples/login-with-axuz/server.mjs (Node 22 ou superior, sem dependências) e um teste contra um provedor simulado em tests/examples/login-with-axuz.test.ts. Ele faz /login (PKCE, state, nonce, discovery), /callback (confere state com o cookie do navegador e iss, troca o code, valida assinatura EdDSA e claims do id_token, chama o userinfo) e mostra o resultado.

export AXUZ_ISSUER=https://auth-api.axuz.net
export AXUZ_CLIENT_ID=SEU_CLIENT_ID
export AXUZ_CLIENT_SECRET=SEU_SECRET   # vazio em client public
export AXUZ_REDIRECT_URI=http://localhost:3000/callback
node examples/login-with-axuz/server.mjs
# abra http://localhost:3000/login

O núcleo do /login é este:

import { createHash, randomBytes } from 'node:crypto';

const b64u = (b) => Buffer.from(b).toString('base64url');
const verifier = b64u(randomBytes(32));            // 43 caracteres
const challenge = b64u(createHash('sha256').update(verifier).digest());
const state = b64u(randomBytes(16));
const nonce = b64u(randomBytes(16));
// guarde verifier, nonce e state (cookie HttpOnly + servidor) e redirecione:
const url = new URL(`${process.env.AXUZ_ISSUER}/oauth/authorize`);
url.search = new URLSearchParams({
  response_type: 'code', client_id: process.env.AXUZ_CLIENT_ID,
  redirect_uri: process.env.AXUZ_REDIRECT_URI, scope: 'openid profile email',
  state, nonce, code_challenge: challenge, code_challenge_method: 'S256',
}).toString();

Segurança: o que não fazer

  • Não use plain, nem implicit, nem coloque o client_secret em app de navegador ou mobile: esses são clients public.
  • Não pule a checagem de state (CSRF de login) nem a de nonce (reuso de id_token).
  • Não aceite id_token sem verificar assinatura, iss, aud e exp; não aceite o alg que o token mandar.
  • Não use e-mail como identidade: ele muda e uma pessoa pode ter vários. Use iss + sub.
  • Não guarde tokens em localStorage nem em log; refresh só no servidor.
  • Não registre redirect_uri genérica: quanto mais exata, menos superfície.

Servidor OAuth antigo (axuz_api)

O axuz_api já expõe GET /oauth/authorize, POST /oauth/token, POST /oauth/revoke, GET /oauth/userinfo, /.well-known/openid-configuration, /.well-known/jwks.json e o cadastro em /v1/oauth/apps, documentados na referência OAuth. Ele difere do IdP novo: os tokens são opacos (axot_, uma hora de validade, refresh de 30 dias), o code vale 10 minutos, o consentimento é por equipe (exige o cookie axuz_active_team, e o sub é derivado do par usuário e equipe, com axuz_user_id e axuz_team_id à parte), e PKCE também é só S256. Além disso, o escopo openid não está no catálogo desse servidor: cadastrar um app com openid hoje responde 400 unknown scope, então o modo OIDC dele não é utilizável por apps de terceiros até esse ajuste. Para “Login com Axuz” novo, planeje contra o IdP do axuz_auth_api.

Atualizado em