OAuth2 e OpenID Connect: guia prático
OAuth2 e OpenID Connect (OIDC) são os protocolos padrão da indústria para delegação de autorização e autenticação federada. Este guia cobre os flows, a anatomia do JWT e exemplos práticos de implementação.
OAuth2 vs OIDC vs SAML
| Protocolo | Propósito | Token | Caso de uso típico |
|---|---|---|---|
| OAuth 2.0 | Autorização (delegação de acesso) | Access Token (opaco ou JWT) | API access, "Login com Google" |
| OpenID Connect | Autenticação (identidade do usuário) | ID Token (JWT) + Access Token | SSO, login federado |
| SAML 2.0 | Autenticação + autorização (enterprise) | XML Assertion | SSO corporativo, Active Directory |
Resumo prático: OAuth2 diz "o usuário me autorizou a acessar X". OIDC adiciona "e aqui está quem é o usuário". SAML é o equivalente XML/enterprise dos dois, amplamente usado em ambientes corporativos legados.
Flows de Autorização
Authorization Code + PKCE (recomendado para SPAs e apps mobile)
PKCE (Proof Key for Code Exchange) elimina a necessidade de client_secret em clientes públicos.
1. App gera code_verifier (aleatório) e code_challenge = SHA256(code_verifier)
2. Redireciona para /authorize com code_challenge
3. Usuário autentica → IdP retorna code na redirect_uri
4. App troca code + code_verifier por tokens no /token endpoint# Passo 1: montar URL de autorização
GET https://auth.example.com/realms/myrealm/protocol/openid-connect/auth
?client_id=my-spa
&response_type=code
&redirect_uri=https://app.example.com/callback
&scope=openid profile email
&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
&code_challenge_method=S256
&state=xyz-csrf-token
# Passo 2: trocar código por tokens
POST https://auth.example.com/realms/myrealm/protocol/openid-connect/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=SplxlOBeZQQYbYS6WxSbIA
&redirect_uri=https://app.example.com/callback
&client_id=my-spa
&code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXkClient Credentials (serviço para serviço)
Usado quando não há usuário envolvido — autenticação machine-to-machine.
POST /realms/myrealm/protocol/openid-connect/token
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials
&client_id=my-service
&client_secret=s3cr3t
&scope=api:read api:writeDevice Code (TVs, CLIs, IoT)
Para dispositivos sem browser ou teclado adequado.
# Passo 1: obter device_code e user_code
POST /protocol/openid-connect/auth/device
client_id=my-cli
# Resposta: { device_code, user_code, verification_uri, expires_in, interval }
# Exibe para o usuário: "Acesse https://auth.example.com/device e insira o código: ABCD-1234"
# Passo 2: poll até o usuário completar
POST /protocol/openid-connect/token
grant_type=urn:ietf:params:oauth:grant-type:device_code
&device_code=GmRhmhcxhwAzkoEqiMEg_DnyEysNkuNhszIySk9eS
&client_id=my-cliImplicit (depreciado)
O flow Implicit retornava tokens diretamente na URL (fragment), expondo-os ao histórico do browser e logs de servidor. Não use em novas aplicações. Use Authorization Code + PKCE no lugar.
Anatomia do JWT
Um JWT tem 3 partes separadas por .: header.payload.signature — cada parte é Base64URL-encoded.
Header
{
"alg": "RS256",
"typ": "JWT",
"kid": "abc123"
}| Campo | Descrição |
|---|---|
alg | Algoritmo de assinatura (RS256, HS256, ES256) |
typ | Tipo do token |
kid | Key ID — identifica qual chave pública verificar em JWKS |
Payload (Claims)
{
"iss": "https://auth.example.com/realms/myrealm",
"sub": "f81d4fae-7dec-11d0-a765-00a0c91e6bf6",
"aud": ["my-api", "account"],
"exp": 1735689600,
"nbf": 1735686000,
"iat": 1735686000,
"jti": "unique-token-id",
"azp": "my-spa",
"scope": "openid profile email",
"email": "usuario@example.com",
"name": "Usuário Exemplo",
"realm_access": {
"roles": ["user", "premium"]
}
}| Claim | Descrição |
|---|---|
iss | Issuer — quem emitiu o token |
sub | Subject — ID único do usuário |
aud | Audience — para quem o token é destinado |
exp | Expiration — timestamp Unix de expiração |
nbf | Not Before — token inválido antes deste timestamp |
iat | Issued At — quando foi emitido |
jti | JWT ID — identificador único para evitar replay |
Signature
RSASHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
privateKey
)A assinatura garante integridade. Com RS256, o IdP assina com chave privada; os serviços verificam com chave pública (obtida via JWKS endpoint).
Validação de Token
import { createRemoteJWKSet, jwtVerify } from 'jose'
const JWKS = createRemoteJWKSet(
new URL('https://auth.example.com/realms/myrealm/protocol/openid-connect/certs')
)
async function validateToken(token: string) {
const { payload } = await jwtVerify(token, JWKS, {
issuer: 'https://auth.example.com/realms/myrealm',
audience: 'my-api',
})
// jose valida automaticamente: assinatura, iss, aud, exp, nbf
return payload
}Checklist de validação manual:
- Assinatura — verificar com a chave pública correta (
kidno header → JWKS) iss— deve corresponder ao seu IdPaud— deve incluir o identificador da sua APIexp— timestamp não pode ser passado (com tolerância de clock skew de ~60s)nbf— timestamp deve ser passadoalg— nunca aceitaralg: "none"— fixar no algoritmo esperado
Refresh Token Rotation + Silent Renew
Rotation (server-side / mobile)
A cada uso do refresh token, um novo par é emitido e o antigo é invalidado. Detecta replay attacks.
async function refreshTokens(refreshToken: string) {
const response = await fetch('/realms/myrealm/protocol/openid-connect/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: refreshToken,
client_id: 'my-app',
}),
})
if (!response.ok) {
// Refresh token inválido/rotacionado — forçar novo login
throw new Error('Session expired')
}
return response.json()
// { access_token, refresh_token (novo!), expires_in, ... }
}Silent Renew (SPA)
Antes de expirar o access token, a SPA abre um iframe oculto para renovar silenciosamente.
// Agendar renovação ~1 minuto antes de expirar
function scheduleTokenRenewal(expiresIn: number) {
const renewalTime = (expiresIn - 60) * 1000
setTimeout(async () => {
try {
await silentRenew()
} catch {
// Sessão expirada — redirecionar para login
redirectToLogin()
}
}, renewalTime)
}Exemplos de Código
Node.js com openid-client
import { Issuer, generators } from 'openid-client'
async function setupOIDC() {
const issuer = await Issuer.discover(
'https://auth.example.com/realms/myrealm'
)
const client = new issuer.Client({
client_id: 'my-app',
client_secret: process.env.CLIENT_SECRET,
redirect_uris: ['http://localhost:3000/callback'],
response_types: ['code'],
})
// Gerar URL de autorização com PKCE
const codeVerifier = generators.codeVerifier()
const codeChallenge = generators.codeChallenge(codeVerifier)
const authUrl = client.authorizationUrl({
scope: 'openid profile email',
code_challenge: codeChallenge,
code_challenge_method: 'S256',
})
return { client, authUrl, codeVerifier }
}
// Callback handler
async function handleCallback(client, callbackUrl, codeVerifier) {
const params = client.callbackParams(callbackUrl)
const tokenSet = await client.callback(
'http://localhost:3000/callback',
params,
{ code_verifier: codeVerifier }
)
const userinfo = await client.userinfo(tokenSet.access_token)
return { tokenSet, userinfo }
}Python com authlib
from authlib.integrations.requests_client import OAuth2Session
from authlib.jose import jwt, JWTClaims
# Client Credentials flow
session = OAuth2Session(
client_id='my-service',
client_secret='s3cr3t',
token_endpoint_auth_method='client_secret_post'
)
token = session.fetch_token(
'https://auth.example.com/realms/myrealm/protocol/openid-connect/token',
grant_type='client_credentials',
scope='api:read'
)
# Validar JWT
from authlib.jose import JsonWebKey
jwks_uri = 'https://auth.example.com/realms/myrealm/protocol/openid-connect/certs'
jwks = requests.get(jwks_uri).json()
key_set = JsonWebKey.import_key_set(jwks)
claims = jwt.decode(token['access_token'], key_set)
claims.validate() # valida exp, iss, aud automaticamentecurl direto
# Obter token via Client Credentials
TOKEN=$(curl -s -X POST \
"https://auth.example.com/realms/myrealm/protocol/openid-connect/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials" \
-d "client_id=my-service" \
-d "client_secret=s3cr3t" \
| jq -r '.access_token')
# Usar o token
curl -H "Authorization: Bearer $TOKEN" https://api.example.com/resource
# Introspectar token (verificar se é válido, sem verificar assinatura localmente)
curl -s -X POST \
"https://auth.example.com/realms/myrealm/protocol/openid-connect/token/introspect" \
-u "my-service:s3cr3t" \
-d "token=$TOKEN" | jq '.active'