Segurança e identidade
OpenID Connect: ID token, discovery, nonce e gerenciamento de sessão
Intermediário8 min de leituraAtualizado
Esta página foi traduzida do inglês com a ajuda de IA e conferida com o original. Ler a versão em inglês
O OpenID Connect transforma o OAuth 2.0, um framework de acesso delegado, em um protocolo de login único ao acrescentar algo que um access token nunca forneceu: uma declaração verificável sobre quem fez login e como. Este guia cobre o ID token e seus claims, o documento de discovery e o conjunto de chaves, os scopes padrão e o UserInfo, contra o que nonce e state protegem, logout e gerenciamento de sessão, e como validar um ID token sem pular etapas.
O que você vai aprender
- Descrever como o OpenID Connect adiciona uma camada de identidade sobre o fluxo authorization code do OAuth 2.0
- Ler um ID token e explicar cada claim obrigatório que ele carrega
- Usar o documento de discovery e o endpoint JWKS em vez de fixar no código a configuração do provedor
- Explicar o que cada um deles impede: nonce, state e os claims c_hash e at_hash
- Validar um ID token corretamente e saber quando usá-lo em vez de um access token
- Escolher entre logout front-channel e back-channel para uma sessão federada
O OAuth 2.0 dá a uma aplicação permissão para chamar uma API. Ele não diz a essa aplicação quem é o usuário, quando ele se autenticou nem se ainda está lá: o access token foi emitido para uma API e pode ser opaco para todos os demais. Mesmo assim, as aplicações precisavam exatamente dessa informação; por isso, durante anos, deduziram a identidade chamando com o access token um endpoint de perfil específico de cada provedor. Essa abordagem tem uma falha real: um access token obtido em qualquer lugar, para qualquer cliente, responde sem problemas a essa chamada.
O OpenID Connect resolve isso definindo um segundo token, endereçado ao próprio cliente. O ID token é um JSON Web Token assinado cuja audiência é a sua aplicação, cujo emissor é o que você fixou e cujos claims descrevem o evento de autenticação. Em torno dessa única ideia, a especificação adiciona discovery, um conjunto padrão de claims e scopes, um endpoint UserInfo e uma semântica de sessão e logout — o suficiente para construir um login único interoperável entre produtos de fornecedores diferentes. Este guia pressupõe que você já conhece o fluxo authorization code do OAuth 2.0; tudo o que vem a seguir é o que o OpenID Connect acrescenta a ele.
O que o protocolo acrescenta
Uma requisição OpenID Connect é uma requisição de autorização OAuth com o scope openid. Esse único scope coloca o provedor em modo OpenID Connect, e a resposta do token endpoint passa a trazer um id_token junto com o access token.
GET /authorize?response_type=code
&scope=openid%20profile%20email
&client_id=web-portal
&redirect_uri=https%3A%2F%2Fportal.example.com%2Fcallback
&state=Rm9vYmFy
&nonce=n-0S6_WzA2Mj
&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
&code_challenge_method=S256 HTTP/1.1
Host: sso.example.com
O vocabulário muda um pouco: o servidor de autorização passa a ser o OpenID Provider (OP) e o cliente, a Relying Party (RP). A resposta do token endpoint parece familiar, com um único acréscimo.
{
"access_token": "IiFwYXNzZWQgdG8gdGhlIEFQSSI",
"token_type": "Bearer",
"expires_in": 300,
"refresh_token": "8xLOxBtZp8",
"id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6InNpZy0yMDI2LTAzIn0.eyJpc3MiOi..."
}
Existem três modos de resposta. O fluxo authorization code é o que você deve usar: tudo o que é sensível volta pelo back channel. O fluxo implícito e o fluxo híbrido devolvem tokens pelo navegador e estão, na prática, aposentados para projetos novos, pelos mesmos motivos que levaram o OAuth a remover o implicit grant.
Por dentro do ID token
Decodificar o payload de um ID token — com um decodificador JWT no lado do cliente, nunca se fiando, no código, em um valor não verificado — revela os claims que carregam todo o significado do protocolo.
{
"iss": "https://sso.example.com",
"sub": "9f2c1b7e-4a55-4f2a-bd58-6f0f1a2c3d44",
"aud": "web-portal",
"exp": 1789012645,
"iat": 1789012345,
"auth_time": 1789012300,
"nonce": "n-0S6_WzA2Mj",
"acr": "urn:mace:incommon:iap:silver",
"amr": ["pwd", "hwk"],
"azp": "web-portal",
"email": "[email protected]",
"email_verified": true,
"name": "Jordan Lee",
"preferred_username": "jordan"
}
iss e sub, juntos, são o único identificador estável de um usuário. O subject é único e nunca é reutilizado dentro de um mesmo emissor, então uma conta local deve ser indexada por esse par, e não por um endereço de email que a pessoa pode mudar a qualquer momento. aud deve ser igual ao seu identificador de cliente; azp indica a parte autorizada quando um token é emitido para várias audiências. auth_time registra quando o usuário de fato se autenticou, e não quando o token foi emitido; é isso que permite exigir um login recente para uma operação sensível enviando max_age ou prompt=login. acr e amr descrevem a força e os métodos dessa autenticação — pwd para senha, otp para código de uso único, hwk para chave de hardware — e são a forma pela qual uma relying party pode exigir autenticação multifator nas telas administrativas.
Os scopes padrão correspondem a conjuntos padrão de claims: profile cobre nome, foto, locale e dados semelhantes; email cobre email e email_verified; address e phone cobrem o que o nome indica. Pedir só o que você precisa mantém a tela de consentimento honesta e reduz os dados pessoais pelos quais você fica responsável.
Discovery e chaves
Nada sobre um provedor deveria ficar fixo no código, exceto a URL do emissor. A partir dela, o documento de discovery, publicado em um caminho conhecido, fornece todos os endpoints e recursos.
GET /.well-known/openid-configuration HTTP/1.1
Host: sso.example.com
{
"issuer": "https://sso.example.com",
"authorization_endpoint": "https://sso.example.com/authorize",
"token_endpoint": "https://sso.example.com/token",
"userinfo_endpoint": "https://sso.example.com/userinfo",
"jwks_uri": "https://sso.example.com/.well-known/jwks.json",
"end_session_endpoint": "https://sso.example.com/logout",
"response_types_supported": ["code"],
"id_token_signing_alg_values_supported": ["RS256", "ES256"],
"scopes_supported": ["openid", "profile", "email", "offline_access"],
"code_challenge_methods_supported": ["S256"]
}
O jwks_uri disponibiliza as chaves públicas de assinatura do provedor como um JSON Web Key Set. Cada chave tem um kid, e o cabeçalho do token indica qual kid foi usado, o que torna a rotação transparente: o provedor publica a nova chave antes de começar a assinar com ela, os clientes buscam o conjunto novamente quando encontram um kid desconhecido, e a chave antiga é retirada depois que os tokens em circulação expiram. Faça cache do conjunto de chaves, respeite os cabeçalhos de cache e limite a frequência das novas buscas, para que um kid desconhecido não possa ser usado para sobrecarregar o provedor.
Nonce, state e os claims de hash
Três mecanismos diferentes protegem contra três fraquezas diferentes, e é comum confundi-los.
state é seu. Você o gera, guarda na sua sessão e compara quando o navegador volta. Ele garante que o callback pertence a um fluxo iniciado por esse navegador, neutralizando cross-site request forgery no endpoint de redirecionamento e o login CSRF, em que um atacante tenta fazer você entrar, sem perceber, na conta dele.
nonce é copiado pelo provedor para dentro do ID token. Ao comparar o nonce do token com o valor que você guardou, você prova que o token foi emitido para esta tentativa de autenticação e não pode ser reaproveitado de outra. Ele é obrigatório nos fluxos implícito e híbrido e fortemente recomendado no fluxo authorization code.
c_hash e at_hash aparecem quando um code ou um access token trafega junto com um ID token pelo front channel. São hashes truncados desses valores, permitindo que a relying party confirme que o code e o token recebidos são os mesmos que o provedor emitiu junto com esse ID token.
PKCE continua valendo por cima de tudo isso: ele protege a própria troca do code e é exigido pelas boas práticas atuais de OAuth, qualquer que seja o tipo de cliente.
Validando um ID token do jeito certo
A validação é uma checklist, e pular qualquer item dela já foi a causa raiz de vulnerabilidades reais. Verifique a assinatura com uma chave do JWKS do provedor selecionada pelo kid, restringindo os algoritmos aceitos aos que o provedor anuncia; confira se iss corresponde exatamente; confira se aud contém o seu identificador de cliente e, se azp estiver presente, se é igual a ele; verifique exp e iat com uma pequena tolerância para diferença de relógio; compare o nonce; e, se você solicitou, verifique auth_time em relação ao seu max_age.
// nimbus-jose-jwt: validator configured once from the discovery document
IDTokenValidator validator = new IDTokenValidator(
new Issuer("https://sso.example.com"),
new ClientID("web-portal"),
JWSAlgorithm.RS256,
new URL("https://sso.example.com/.well-known/jwks.json"));
try {
IDTokenClaimsSet claims = validator.validate(SignedJWT.parse(idToken), expectedNonce);
String subject = claims.getSubject(); // stable only together with the issuer
Date authenticatedAt = claims.getAuthenticationTime();
if (authenticatedAt.toInstant().isBefore(Instant.now().minus(Duration.ofMinutes(15)))) {
throw new ReauthenticationRequiredException(); // step-up for a sensitive action
}
} catch (BadJOSEException | JOSEException e) {
throw new AuthenticationFailedException("id_token rejected", e);
}
Só escreva isso você mesmo se não houver alternativa. Uma biblioteca — ou o cliente OpenID Connect embutido no Spring Security — implementa a lista completa, incluindo as restrições de algoritmo e o tratamento da rotação de chaves que o código escrito à mão costuma omitir. As armadilhas da verificação de qualquer JWT, incluindo a confusão de algoritmos, são tratadas em protegendo APIs REST com JWT.
UserInfo, e qual token vai para onde
O endpoint UserInfo devolve claims sobre o usuário autenticado mediante um access token válido que tenha o scope openid.
GET /userinfo HTTP/1.1
Host: sso.example.com
Authorization: Bearer IiFwYXNzZWQgdG8gdGhlIEFQSSI
Ele é útil quando os claims mudam durante a sessão ou quando você mantém o ID token pequeno de propósito, mas custa uma chamada de rede, e a resposta precisa ter um sub idêntico ao do ID token — caso contrário, houve uma substituição de token. Para tudo de que você precisa em toda requisição, prefira os claims do ID token.
Vale deixar a divisão de papéis bem explícita, porque errar aqui é o erro de design mais comum com OpenID Connect:
| Token | Audiência | Finalidade | Enviado para |
|---|---|---|---|
| ID token | O seu cliente | Quem fez login, como e quando | Lugar nenhum — consumido pelo cliente |
| Access token | Uma API | O que o portador pode fazer lá | O servidor de recursos |
| Refresh token | O provedor | Obter novos tokens de forma silenciosa | Apenas o token endpoint |
Sessões e logout
Um login federado cria pelo menos duas sessões: uma no provedor e uma em cada aplicação. Encerrá-las de forma coerente é a parte do login único que as implementações mais deixam de lado.
O logout iniciado pela RP envia o usuário para o end_session_endpoint do provedor com id_token_hint e uma post_logout_redirect_uri, encerrando a sessão no provedor e levando o usuário de volta a uma página que você registrou previamente.
No front-channel logout, o provedor carrega em um iframe oculto uma URL de logout para cada aplicação. É simples, mas depende de cookies de terceiros e de que cada iframe carregue com sucesso, por isso falha silenciosamente nos navegadores modernos.
O back-channel logout é a opção confiável. O provedor envia via POST um logout token assinado — um JWT com um sid ou sub e um claim events que identifica o evento de logout — diretamente para cada cliente registrado, de servidor para servidor.
POST /backchannel-logout HTTP/1.1
Host: portal.example.com
Content-Type: application/x-www-form-urlencoded
logout_token=eyJhbGciOiJSUzI1NiIsImtpZCI6InNpZy0yMDI2LTAzIn0...
Valide esse token exatamente como valida um ID token, com a regra extra de que ele não pode conter um nonce, e depois destrua a sessão local identificada pelo sid. Como isso exige controle por sessão, aplicações que dependem apenas de cookies stateless precisam adicionar um índice de sessões no servidor — um preço justo por um logout que realmente funciona. Se você estiver integrando com um provedor corporativo mais antigo, o mecanismo equivalente lá é o single logout do SAML, que enfrenta as mesmas dificuldades de forma mais complicada.
Principais pontos
- OpenID Connect é OAuth 2.0 mais o scope
openid, um ID token endereçado ao seu cliente e claims de identidade padronizados. - O ID token diz quem se autenticou, quando e com quais métodos;
issesubjuntos são a única chave de usuário estável. - Leia todos os endpoints e chaves do documento de discovery e do JWKS, para que a rotação e as mudanças no provedor não exijam novo deploy.
statevincula o redirecionamento à sua sessão,noncevincula o ID token a esta tentativa de login e o PKCE vincula o code ao seu cliente — use os três.- Valide assinatura, emissor, audiência, expiração, nonce e, quando for o caso,
auth_time; use uma biblioteca em vez de verificações escritas à mão. - Envie access tokens para as APIs, mantenha os ID tokens no cliente e implemente o back-channel logout se o logout único precisar ser confiável.
Fontes e leituras complementares
Perguntas frequentes
- Devo enviar o ID token para a minha API?
- Não. O ID token é destinado à aplicação cliente — a audiência dele é o seu identificador de cliente — e existe para informar a essa aplicação quem fez login. APIs devem aceitar access tokens, cuja audiência identifica a API e cujos scopes descrevem o que o chamador pode fazer. Enviar um ID token para uma API obriga a API a aceitar um token cujos scopes nunca contemplaram essa API, o que quebra a validação da audiência.
- Qual é a diferença entre nonce e state?
- O state protege o redirecionamento de volta para a sua aplicação: você o gera, guarda na sessão e compara no retorno, o que impede um atacante de iniciar um fluxo que termine no navegador de uma vítima. O nonce protege o próprio ID token: o provedor o copia para dentro do token, então um token reaproveitado de outra tentativa de login falha na comparação. Na prática, os dois são necessários.
- Posso confiar no claim email de qualquer provedor?
- Somente quando email_verified for true e você confiar que aquele emissor específico fez a verificação. Associar contas com base em um email não verificado é um caminho conhecido de sequestro de conta, porque um usuário pode registrar em um provedor pouco rigoroso um endereço que não controla. Identifique as contas locais pelo par emissor e subject, e trate o email como um atributo de exibição que você pode atualizar após a verificação.
- Como sei quando uma sessão federada terminou?
- Não dá para deduzir isso pela expiração dos tokens. O back-channel logout é o mecanismo confiável: o provedor envia via POST um logout token assinado para cada cliente que registrou um endpoint de logout, e o cliente destrói a sessão local correspondente. O front-channel logout usa iframes ocultos; é mais fácil de implantar, mas pouco confiável quando cookies de terceiros ou a rede estão restritos.