Documentação
Servidor OAuthTokens

Tokens

O que seu projeto emite, o que tem dentro de cada token, e como um resource server valida sem chamar a gente.

Um fluxo completo produz até três tokens. Eles não são intercambiáveis, e usar o errado é o bug de integração mais comum.

access_tokenpara chamar APIsOptional

Enviado como Authorization: Bearer …. Diz o que o portador pode fazer.

id_tokenpara saber quem entrouOptional

Emitido só com o escopo openid. Diz quem é o usuário. Nunca mande para uma API como credencial — é uma afirmação sobre identidade, não uma concessão de acesso.

refresh_tokenpara obter novo access tokenOptional

Vida longa, sem interação do usuário. Guarde como guardaria uma senha.

O emissor

Tudo se ancora em um valor: o emissor, o endereço público do servidor de autorização do seu projeto.

https://seu-projeto.nulvless.co/auth

Ele aparece como iss em todo token, e todos os endpoints do documento de discovery são construídos a partir dele.

Ele precisa casar com onde o discovery é servido

Isso não é regra nossa, é da especificação. O OpenID Connect Discovery §4.3 exige que o issuer do documento seja idêntico à URL que o cliente usou como prefixo para buscá-lo:

cliente recebe      https://seu-projeto.nulvless.co/auth
cliente busca       https://seu-projeto.nulvless.co/auth/.well-known/openid-configuration
documento responde  { "issuer": "https://seu-projeto.nulvless.co/auth", … }
                                 └── as duas precisam ser a mesma string

Se divergirem, um cliente conforme rejeita o provedor — e é por isso que o emissor não é uma configuração. Existe exatamente um valor que funciona: a URL pública onde o seu projeto responde, que é a que o Console mostra. Ele é composto, não escolhido, então não há como digitá-lo errado.

Um domínio personalizado não o muda: o emissor continua na URL do projeto.

`.well-known` é só um caminho reservado

A RFC 8615 reserva /.well-known/ para metadados que máquinas leem. Não há nada de especial nele além da convenção — é por isso que entregar uma única URL ao cliente, o emissor, basta para ele achar todo o resto.

Ele não é onde a sua tela de consentimento mora

Três URLs diferentes aparecem em um mesmo fluxo, e confundi-las é a fonte usual de dúvida.

ExemploDe quem éPara que serve
Emissorhttps://acme.nulvless.co/authnulvlessOnde o protocolo vive: discovery, authorize, token, JWKS
Site URL + caminho de autorizaçãohttps://acme.com/oauth/consentvocêOnde a pessoa decide. A página que você constrói
Redirect URIhttps://analytics.io/callbacko app de terceiroPara onde o código vai no fim

Sua tela de consentimento morar no seu próprio domínio não muda o emissor. São camadas diferentes, e o protocolo só conhece a primeira.

Na verdade a tela de consentimento é invisível para o OAuth. Não é um endpoint, não tem nome na especificação e nunca aparece no documento de discovery. Do ponto de vista do app de terceiro, ele mandou um usuário para o seu endpoint de autorização e o usuário voltou ao callback dele com um código. O que aconteceu no meio é assunto seu.

Seguindo uma requisição:

1. analytics.io  →  acme.nulvless.co/auth/oauth/authorize?…      emissor
2. acme.nulvless.co  →  acme.com/oauth/consent?authorization_id=…  seu Site URL
3. acme.com  →  acme.nulvless.co/auth/oauth/authorizations/…     emissor de novo
4. acme.com  →  analytics.io/callback?code=…                     redirect URI
5. analytics.io  →  acme.nulvless.co/auth/oauth/token            emissor de novo

Mais duas consequências

  • Não é a URL pela qual a requisição chegou. É configuração, então o mesmo projeto alcançado por outro caminho emite o mesmo iss.
  • Cada ambiente tem o seu. Um token de staging carrega emissor de staging e não valida em produção, que é exatamente o que se quer.

Discovery

Todo cliente OpenID Connect começa aqui, e a maioria das bibliotecas faz isso por você se receber o emissor:

GET {issuer}/.well-known/openid-configuration
{
  "issuer": "https://seu-projeto.nulvless.co/auth",
  "authorization_endpoint": "…/oauth/authorize",
  "token_endpoint": "…/oauth/token",
  "userinfo_endpoint": "…/oauth/userinfo",
  "jwks_uri": "…/.well-known/jwks.json",
  "response_types_supported": ["code"],
  "grant_types_supported": ["authorization_code", "refresh_token"],
  "code_challenge_methods_supported": ["S256"],
  "id_token_signing_alg_values_supported": ["RS256"]
}

Leia os endpoints daqui em vez de fixá-los no código. É a razão inteira de o documento existir.

O documento e o JWKS são públicos e cacheáveis — carregam Cache-Control: public, max-age=… de propósito. Toda outra resposta do servidor de autorização carrega no-store.

Validando um ID token

ID tokens são assinados com RS256, então sua API valida localmente, contra as chaves públicas em jwks_uri. Sem chamada de rede à nulvless no caminho quente.

O access token é outra coisa: ele é assinado com o secret do seu projeto, não com a chave publicada em jwks_uri. Não tente validá-lo contra o JWKS — use o ID token quando precisar estabelecer quem é o usuário, e o userinfo quando precisar das claims depois.

import { createRemoteJWKSet, jwtVerify } from 'jose';

const issuer = 'https://seu-projeto.nulvless.co/auth';
const jwks = createRemoteJWKSet(new URL(`${issuer}/.well-known/jwks.json`));

export async function verifyIdToken(idToken: string) {
  const { payload } = await jwtVerify(idToken, jwks, {
    issuer,
    audience: 'sua-api',
  });

  return payload;
}

Quatro coisas precisam ser checadas, e uma biblioteca que pule qualquer uma não está validando:

Assinaturacontra o jwks_uriOptional

Prova que o seu projeto emitiu.

issigualdade exataOptional

Prova qual projeto e ambiente. Pular isso deixa passar token de outro tenant.

audigualdade exataOptional

Prova que o token era para a sua API e não repassado de outra.

expnão vencidoOptional

Prova que ainda vale.

Rotação de chave

O jwtVerify escolhe a chave pelo kid do cabeçalho, então a rotação é transparente — desde que seu cache do JWKS não seja mais velho que a chave nova. O jose busca de novo diante de um kid desconhecido. Se você mesmo cacheia o JWKS, faça o mesmo, e nunca cacheie para sempre.

Lendo o ID token

O ID token carrega as claims dos escopos concedidos.

ClaimConcedida por
subsempre — o id estável do usuário
iss, aud, exp, iatsempre
nonceecoada da requisição de autorização
email, email_verifiedemail
name, pictureprofile
phone_number, phone_number_verifiedphone

Valide do mesmo jeito, e confira também que o nonce bate com o que você mandou. sub é o que você guarda como chave estrangeira da conta — não o e-mail, que muda.

Userinfo

As mesmas claims, buscadas com o access token, para quando você as quer frescas em vez de como estavam no login:

curl {issuer}/oauth/userinfo -H "Authorization: Bearer $ACCESS_TOKEN"

Use o ID token quando precisar da identidade no login. Use userinfo quando precisar depois e o perfil possa ter mudado.

Expiração e renovação

Access tokens têm vida curta; expires_in volta junto com o token. Renove antes de vencer em vez de esperar um 401:

const { data } = await nulvless.auth.oauth.refreshToken({ refreshToken, clientId });

Se a renovação falhar, a concessão acabou — o usuário revogou, ou um administrador revogou. Isso é um pedido de login novo, não uma tentativa de repetir.

Próximo

Nesta página