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:

TokenAudiênciaFinalidadeEnviado para
ID tokenO seu clienteQuem fez login, como e quandoLugar nenhum — consumido pelo cliente
Access tokenUma APIO que o portador pode fazer láO servidor de recursos
Refresh tokenO provedorObter novos tokens de forma silenciosaApenas 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; iss e sub juntos 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.
  • state vincula o redirecionamento à sua sessão, nonce vincula 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.