OAuth 2.0, especificado na RFC 6749 em 2012, nasceu para resolver um problema específico: como um usuário pode conceder a um aplicativo de terceiro acesso limitado a seus recursos em outro serviço sem compartilhar suas credenciais. O caso de uso original era "permite que este app de fotos acesse meu álbum do Google Fotos" — delegação de autorização sem compartilhamento de senha. A solução elegante que o OAuth propôs — um token de acesso limitado no escopo e no tempo, emitido por um authorization server após consentimento explícito do usuário — tornou-se a base da identidade federada moderna.
O problema é que o OAuth 2.0 original era um framework de extensão, não um protocolo completo. As RFC 6749 e 6750 definiam os componentes mas deixavam muitas decisões em aberto: como verificar o PKCE? Quais grant types são seguros em quais contextos? Como revogar tokens? Dez anos de implementação produziram um ecossistema fragmentado e, frequentemente, inseguro. OAuth 2.1 — publicado como draft da IETF em 2023 — é a consolidação desse aprendizado: mantém o que funciona, remove os grant types inseguros (Implicit, Resource Owner Password Credentials), e torna obrigatório o que antes era opcional (PKCE, HTTPS).
OpenID Connect (OIDC), publicado em 2014 pela OpenID Foundation como camada sobre OAuth 2.0, adiciona o que o OAuth nunca foi projetado para fazer: autenticação. OAuth 2.0 cuida de autorização — acesso a recursos. OIDC adiciona a identidade do usuário via ID token, um JWT com claims padronizadas sobre quem o usuário é. Juntos, OAuth 2.1 + OIDC formam a base de praticamente todo sistema moderno de "Login com Google/GitHub/Microsoft".
Os quatro papéis do OAuth
Para entender qualquer flow OAuth, é necessário ter os quatro papéis claros:
- Resource Owner: o usuário que possui os dados e concede acesso. Geralmente uma pessoa, mas pode ser um sistema em flows machine-to-machine.
- Client: o aplicativo que quer acesso aos dados do resource owner. Pode ser uma SPA, um app mobile, um servidor de backend, ou outro serviço.
- Authorization Server: o servidor que emite tokens após verificar identidade e obter consentimento. Exemplos: Keycloak, Auth0, Okta, Google Identity, Azure AD.
- Resource Server: o servidor que hospeda os dados protegidos e aceita access tokens. Sua API, o Google Drive, o GitHub API.
Um mesmo sistema pode ter vários desses papéis coincidindo: em um sistema de autenticação proprietário, o authorization server e o resource server são frequentemente o mesmo serviço.
Authorization Code Flow com PKCE — passo a passo
O Authorization Code Flow é o flow correto para aplicações que podem manter um segredo (aplicações server-side) e, com PKCE (Proof Key for Code Exchange, RFC 7636), também para SPAs e apps mobile que não podem. PKCE foi originalmente desenhado para apps mobile (onde o client_secret não pode ser verdadeiramente secreto), mas o OAuth 2.1 o torna obrigatório para todos os clientes públicos.
O mecanismo completo em sequência:
// Passo 1: Cliente gera code_verifier e code_challenge (PKCE)
const codeVerifier = base64url(crypto.getRandomValues(new Uint8Array(32)));
const codeChallenge = base64url(await crypto.subtle.digest(
'SHA-256',
new TextEncoder().encode(codeVerifier)
));
// Armazenar codeVerifier na sessão do cliente (não na URL)
// Passo 2: Redirecionar usuário ao Authorization Server
const authUrl = new URL('https://auth.app.com/oauth/authorize');
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('client_id', 'spa-client');
authUrl.searchParams.set('redirect_uri', 'https://app.com/callback');
authUrl.searchParams.set('scope', 'openid profile email');
authUrl.searchParams.set('state', generateRandomState()); // CSRF protection
authUrl.searchParams.set('code_challenge', codeChallenge);
authUrl.searchParams.set('code_challenge_method', 'S256');
window.location.href = authUrl.toString();
// → Usuário autentica no Authorization Server e consente aos escopos
// Passo 3: Authorization Server redireciona de volta com authorization code
// https://app.com/callback?code=AUTH_CODE_AQUI&state=STATE_AQUI
// Passo 4: Verificar state (CSRF) e trocar code por tokens
// (feito no servidor, não no browser, para clientes confidenciais)
const response = await fetch('https://auth.app.com/oauth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
code: authorizationCode,
redirect_uri: 'https://app.com/callback',
client_id: 'spa-client',
code_verifier: codeVerifier, // prova que é o mesmo cliente do passo 1
// client_secret: apenas para clientes confidenciais (server-side)
}),
});
const { access_token, id_token, refresh_token, expires_in } = await response.json();
O papel do PKCE é proteger contra interceptação do authorization code. Sem PKCE, um app malicioso que consegue interceptar o redirect (em apps mobile via URI scheme registrado por outro app, ou em browsers via extensões maliciosas) recebe o authorization code e pode trocá-lo por tokens. Com PKCE, o code sozinho é inútil sem o code_verifier correspondente — que só o cliente original tem, armazenado em memória de sessão.
O state parameter não é opcional — é proteção contra CSRF. Sem ele, um atacante pode fazer o usuário completar um flow OAuth no servidor do atacante e então redirecionar o browser da vítima para o callback do cliente legítimo com o authorization code do atacante — associando a conta do atacante à sessão da vítima. O state deve ser aleatório, único por request, e verificado antes de trocar o code por tokens.
Client Credentials Flow — autenticação machine-to-machine
Quando não há usuário humano — serviço A chamando API do serviço B — o flow correto é Client Credentials. O cliente usa suas próprias credenciais (client_id + client_secret) para obter um access token diretamente, sem redirect, sem usuário, sem authorization code.
# Python — Client Credentials Flow
import httpx
async def get_service_token() -> str:
response = await httpx.AsyncClient().post(
'https://auth.internal/oauth/token',
data={
'grant_type': 'client_credentials',
'client_id': settings.CLIENT_ID,
'client_secret': settings.CLIENT_SECRET,
'scope': 'reports:read notifications:write',
}
)
response.raise_for_status()
token_data = response.json()
# Cache o token até próximo de expirar — evitar round-trip por requisição
expires_at = time.time() + token_data['expires_in'] - 30 # 30s de margem
_token_cache.set(token_data['access_token'], expires_at)
return token_data['access_token']
O client_secret em um flow machine-to-machine vive no servidor — diferente de SPAs e apps mobile, onde não há servidor para guardar segredos. Para ambientes cloud modernos, a alternativa mais segura ao client_secret é autenticação por certificate (JWT bearer assertion, RFC 7523): o cliente assina uma asserção JWT com sua chave privada, e o authorization server verifica com a chave pública cadastrada. Sem segredo compartilhado, sem risco de vazamento de segredo.
OpenID Connect — identidade sobre OAuth
OAuth 2.0 foi projetado para autorização, não para autenticação. O access token é opaco para o cliente — ele não deve tentar ler o conteúdo, apenas usá-lo como credential para acessar o resource server. O problema: times queriam usar OAuth para login, e a pergunta "mas quem é o usuário?" não tinha resposta padronizada no OAuth puro.
OpenID Connect resolve isso adicionando um ID token — um JWT com claims padronizadas sobre o usuário — ao response do token endpoint. Claims obrigatórias no ID token: iss (issuer), sub (subject — o identificador estável do usuário no authorization server), aud (audience — deve conter o client_id), exp (expiração), iat (emitido em). Claims opcionais mas comuns: email, name, picture, email_verified.
// ID token decodificado (apenas header.payload — assinatura omitida)
{
"iss": "https://auth.app.com",
"sub": "user_abc123", // identificador estável — use como FK no seu banco
"aud": "spa-client", // deve ser o client_id da sua aplicação
"exp": 1716321600,
"iat": 1716318000,
"email": "user@example.com",
"email_verified": true,
"name": "Camila Costa",
"picture": "https://auth.app.com/avatars/abc123.jpg",
"nonce": "NONCE_DA_REQUISICAO" // bind entre request e token
}
O sub (subject) é o campo crítico para armazenamento. O email pode mudar; o nome certamente muda; mas o sub é o identificador estável e imutável do usuário no authorization server. Ao criar o usuário no seu banco na primeira vez que ele faz login via OIDC, armazene sub + iss como a chave — não o email.
OIDC também define o UserInfo endpoint — um endpoint no authorization server que retorna informações do usuário mediante apresentação de access token com escopo openid profile email. Útil para obter claims mais recentes sem requerer novo login (o ID token captura o estado no momento do login, o UserInfo reflete o estado atual).
Discovery e JWKS — como clientes encontram a configuração
OIDC define o Discovery document (RFC 8414) — um endpoint .well-known/openid-configuration que todo authorization server compatível expõe. O documento lista endpoints, algoritmos suportados, escopos disponíveis, e a URL do JWKS endpoint.
# Exemplo de discovery document — Google
GET https://accounts.google.com/.well-known/openid-configuration
{
"issuer": "https://accounts.google.com",
"authorization_endpoint": "https://accounts.google.com/o/oauth2/v2/auth",
"token_endpoint": "https://oauth2.googleapis.com/token",
"userinfo_endpoint": "https://openidconnect.googleapis.com/v1/userinfo",
"jwks_uri": "https://www.googleapis.com/oauth2/v3/certs",
"scopes_supported": ["openid", "email", "profile", ...],
"response_types_supported": ["code", "token", "id_token token", ...],
"id_token_signing_alg_values_supported": ["RS256"],
"claims_supported": ["sub", "iss", "aud", "exp", "iat", "email", "name", ...]
}
O JWKS endpoint (jwks_uri) retorna as chaves públicas do authorization server em formato JSON Web Key Set. O cliente usa essas chaves para verificar a assinatura dos ID tokens e access tokens JWT. Boas bibliotecas OIDC (como openid-client em Node.js ou authlib em Python) fazem o discovery e a verificação de assinatura automaticamente — o desenvolvedor não deve implementar isso manualmente.
Não fazer cache do JWKS localmente com TTL adequado causa dois problemas: lentidão (uma requisição ao JWKS endpoint por token verificado) e fragilidade (se o JWKS endpoint ficar indisponível, a autenticação para). A prática correta é cache com TTL de 1-6 horas com invalidação forçada quando um token tem um kid (key ID) não presente no cache — permitindo rotação de chaves sem indisponibilidade.
Segurança de OAuth — os ataques e as defenses
A RFC 9700 (OAuth 2.0 Security Best Current Practice, 2023) é o documento normativo sobre o que pode dar errado em implementações OAuth. Os principais vetores:
- CSRF no callback: mitigado pelo state parameter verificado antes de trocar o code. Sem state, um atacante pode iniciar um flow OAuth, capturar o redirect URL com o code, e fazer a vítima completar o flow — associando a conta do atacante.
- Authorization code injection: um code legítimo obtido por um cliente malicioso é injetado no callback de um cliente legítimo. Mitigado pelo PKCE — o code só pode ser trocado por quem tem o code_verifier.
- Open redirects no redirect_uri: o authorization server deve validar o redirect_uri contra a lista de URIs pré-registradas exatamente (não por prefixo). Um redirect_uri com open redirect (ex:
https://app.com/callback?next=https://evil.com) pode ser explorado para desviar o authorization code. - Token leakage via referrer: tokens na URL (access token no fragment, code no query string) podem vazar via Referer header em navegação subsequente. Por isso access tokens não devem aparecer em URLs — o flow correto os transmite no body da resposta do token endpoint.
- Mix-up attacks: em clientes que usam múltiplos authorization servers, um servidor malicioso pode enganar o cliente a enviar tokens para o servidor errado. Mitigado por
issbinding — verificar que oissno response corresponde ao servidor esperado.
Identity providers na prática — Keycloak, Auth0, Okta
Para a maioria dos sistemas, implementar um authorization server do zero é a escolha errada — o espaço de falhas é enorme e o valor agregado é zero. A decisão é entre soluções self-hosted e soluções SaaS.
Keycloak (Red Hat, open source) é a opção self-hosted de referência. Suporta OAuth 2.1, OIDC, SAML 2.0, social login, MFA com TOTP e WebAuthn, e tem integração nativa com Kubernetes via Keycloak Operator. O custo é operacional — Keycloak é um sistema Java com requisitos de memória e HA. Para times com capacidade de operar infraestrutura complexa e requisitos de dados em jurisdição específica, é a escolha.
Auth0 (Okta) e Okta são SaaS que eliminam a complexidade operacional ao custo de vendor lock-in e custo por usuário ativo mensal. Para startups e times sem operação de identidade, o custo de Auth0 até ~10k usuários é frequentemente menor que o custo de engenharia de operar Keycloak.
Para sistemas internos (M2M, APIs internas), uma alternativa mais leve é Dex (CNCF project) — um authorization server OIDC minimalista que delega autenticação a provedores upstream (GitHub, LDAP, Google). Ideal para autenticar serviços internos contra o diretório corporativo existente.
Como praticar
-
Implementar o Authorization Code Flow com PKCE do zero. Suba um Keycloak local via Docker (
docker run -p 8080:8080 quay.io/keycloak/keycloak:latest start-dev), crie um realm, um client público, e um usuário de teste. Implemente o flow completo em qualquer linguagem: geração de code_verifier/challenge, redirect, callback, troca de code por tokens, verificação do ID token. Use a biblioteca OIDC da linguagem para a verificação — não implemente a crypto manualmente.
Critério: Flow completo sem atalhos — geração de code_verifier criptograficamente seguro (32 bytes aleatórios), verificação do state antes de trocar o code, validação do ID token (iss, aud, exp) com chaves do JWKS endpoint. Conseguir fazer login com um usuário de teste e exibir o sub do ID token. -
Inspecionar o que o authorization server retorna. Com Keycloak rodando, use o JWT debugger (jwt.io) para decodificar o ID token e o access token recebidos. Identifique: qual claim é o identificador do usuário? Qual é o algoritmo de assinatura? Quais escopos aparecem no access token? Verifique o discovery document em
http://localhost:8080/realms/master/.well-known/openid-configuratione o JWKS endpoint listado nele.
Critério: Responder com segurança: qual claim usar como chave primária do usuário no banco de dados, por que não o email, e o que diferencia visualmente um access token de um ID token no JWT decodificado. Verificar a assinatura do ID token com a chave pública do JWKS endpoint. -
Implementar Client Credentials com cache de token. Crie um segundo client no Keycloak do tipo confidential (com client_secret), configure escopos de serviço, e implemente uma chamada de serviço a serviço que obtém token via Client Credentials, faz cache até 30 segundos antes da expiração, e usa o token para chamar uma API protegida. Logue o timestamp de cada chamada ao token endpoint vs. chamadas à API protegida.
Critério: Cache funcionando — número de requisições ao token endpoint deve ser 1 para N chamadas à API dentro da validade do token. Log mostrando diferença de latência entre primeira chamada (obtém token) e subsequentes (usa cache). -
Explorar ataques OAuth no Portswigger Web Security Academy. Complete pelo menos 2 labs de OAuth: "OAuth account hijacking via implicit flow" e "Forced OAuth profile linking". Para cada lab, documente: qual foi a vulnerabilidade explorada, qual medida de segurança estava ausente, e como o OAuth 2.1 ou PKCE teria prevenido o ataque.
Critério: Para cada lab: a explicação do ataque em suas próprias palavras (não copiar o enunciado), o parâmetro ou header específico que estava ausente no servidor vulnerável, e a mudança concreta de código ou configuração que fecharia a vulnerabilidade. -
Configurar SSO entre dois clientes no mesmo Keycloak. Configure um segundo client no realm do exercício 1. Faça login no primeiro client, depois acesse uma URL protegida do segundo client — com SSO ativo, o segundo client deve reconhecer o usuário sem novo prompt de login. Experimente ajustar SSO Session Max e Access Token Lifespan; observe a diferença entre sessão SSO expirada (novo login) e token expirado (refresh silencioso).
Critério: SSO funcionando — login no client-A, acesso ao client-B sem prompt de login. Console de admin do Keycloak mostrando a sessão ativa com os dois clients. Documentar a diferença entre session timeout (SSO) e token expiration (access token).
Perguntas de entrevista
Qual a diferença entre OAuth 2.0 e OpenID Connect? Se alguém diz que está "usando OAuth para login", por que isso tecnicamente está errado?
OAuth é um protocolo de autorização — ele delega acesso a recursos sem expor credenciais, mas não define quem é o usuário; o access token é opaco para o cliente. OpenID Connect é uma camada de autenticação sobre OAuth 2.0 que adiciona o ID token — um JWT com claims padronizadas sobre quem o usuário é (sub, email, name). "Usar OAuth para login" está tecnicamente errado porque sem OIDC o cliente não tem forma padronizada de saber quem autenticou. Muitas implementações "fazem login com OAuth" lendo um endpoint do resource server (ex: GitHub
/user) — funciona, mas cria acoplamento ao provider específico e não segue o padrão de interoperabilidade do OIDC.Explique o mecanismo do PKCE. Que ataque ele mitiga e por que o state parameter sozinho não é suficiente?
PKCE (RFC 7636): o cliente gera um
code_verifieraleatório (32+ bytes), calculacode_challenge = base64url(SHA256(code_verifier)), e envia o challenge na authorization request. Na troca do code por tokens, envia o verifier original. O authorization server verifica queSHA256(verifier) == challengearmazenado — provando que é o mesmo cliente que iniciou o flow. Mitiga interceptação do authorization code: um atacante que captura o code (via URI scheme de outro app mobile, extensão maliciosa, ou log de proxy) não consegue trocá-lo por tokens sem o verifier, que nunca sai do cliente. O state mitiga CSRF — garante que a authorization response pertence à request iniciada pela mesma sessão — mas não impede que um atacante que obteve o code o use. São complementares, não substitutos.Você está construindo uma CLI que precisa chamar uma API protegida por OAuth em nome do usuário. Qual flow OAuth usar e por quê?
Duas opções válidas: (1) Device Authorization Grant (RFC 8628) — a CLI exibe um código curto e uma URL; o usuário acessa no browser em qualquer dispositivo, autentica e aprova; a CLI faz polling ao token endpoint até receber o access token. Ideal quando a CLI não pode abrir um browser. (2) Authorization Code + PKCE com localhost redirect — a CLI abre o browser do sistema operacional com a authorization URL e inicia um servidor HTTP temporário em porta aleatória em localhost; o redirect_uri aponta para
http://127.0.0.1:{porta}/callback. Após o usuário autenticar, a CLI captura o code no callback. Esse é o padrão do GitHub CLI, AWS CLI v2 e kubectl. ROPC (Resource Owner Password Credentials) — enviar usuário e senha diretamente para a CLI — foi removido do OAuth 2.1 por eliminar a separação de credenciais que é o ponto central do protocolo.Um ID token JWT chega na sua aplicação. Quais claims você valida obrigatoriamente antes de confiar no token?
Validação obrigatória segundo a spec OIDC: (1) Assinatura — verificar com a chave pública do JWKS endpoint do issuer, usando o kid (key ID) do header para selecionar a chave certa. Nunca pular. (2) iss — deve ser exatamente o authorization server esperado (comparação exata de string, não contains). Previne aceitar tokens de outros issuers. (3) aud — deve conter o client_id da sua aplicação. Previne audience confusion attack onde um token emitido para outro cliente é aceito. (4) exp — deve ser no futuro, com tolerância mínima de clock skew (segundos, não minutos). (5) nonce — se enviado na authorization request, deve estar presente e igual no token (previne replay de tokens antigos). O algoritmo de assinatura deve ser explicitamente verificado contra uma allowlist — nunca aceitar
alg: noneou algoritmos simétricos quando se espera RSA/EC.Dois microsserviços internos precisam se comunicar com autenticação. Quando usar mTLS vs Client Credentials OAuth?
Client Credentials OAuth: serviço A obtém access token do authorization server e apresenta ao serviço B. Vantagens: scopes granulares (A pode acessar apenas o que precisa), auditoria centralizada no authorization server, revogar acesso de um serviço é uma operação administrativa sem code deploy. Custo: latência adicional na obtenção do token (mitigada por cache) e dependência do authorization server na disponibilidade. mTLS: cada serviço tem certificado de cliente; autenticação ocorre na camada TLS sem round-trip ao authorization server. Vantagens: zero latência adicional, funciona independente do authorization server, funciona em qualquer protocolo TCP. Custo: requer PKI interna, rotação de certificados deve ser automatizada (cert-manager, Vault PKI), não tem scopes nativos. Decisão prática: em Kubernetes com service mesh (Istio/Linkerd), mTLS automático entre pods é o padrão operacionalmente mais simples — sem código adicional. Para comunicação cross-cluster, cross-cloud, ou com sistemas externos onde scopes e auditoria granular importam, Client Credentials escala melhor.
Referências para aprofundar
- book OAuth 2 in Action — Richer e Sanso (Manning, 2017).
- docs RFC 6749 — The OAuth 2.0 Authorization Framework — IETF.
- docs OAuth 2.1 Draft — The OAuth 2.1 Authorization Framework — IETF.
- docs OpenID Connect Core 1.0 — OpenID Foundation.
- docs RFC 7636 — Proof Key for Code Exchange (PKCE) — IETF.
- docs RFC 9700 — OAuth 2.0 Security Best Current Practice — IETF.
- article OAuth 2.0 Threat Model and Security Considerations (RFC 6819) — IETF (2013).
- docs Keycloak — Documentação Oficial — Red Hat.
- article An Illustrated Guide to OAuth and OpenID Connect — David Gilbertson (medium.com, 2020).
- vídeo OAuth 2.0 and OpenID Connect (in plain English) — OktaDev (YouTube, 2019).
- article Mix-Up Attacks on OAuth — Fett, Küsters, Schmitz (IEEE S&P 2016).
- book API Security in Action — Neil Madden (Manning, 2020).