Kaique Mitsuo Silva Yamamoto
Arquitetura softwareSeguranca

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

ProtocoloPropósitoTokenCaso de uso típico
OAuth 2.0Autorização (delegação de acesso)Access Token (opaco ou JWT)API access, "Login com Google"
OpenID ConnectAutenticação (identidade do usuário)ID Token (JWT) + Access TokenSSO, login federado
SAML 2.0Autenticação + autorização (enterprise)XML AssertionSSO 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_wW1gFWFOEjXk

Client 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:write

Device 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-cli

Implicit (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.

{
  "alg": "RS256",
  "typ": "JWT",
  "kid": "abc123"
}
CampoDescrição
algAlgoritmo de assinatura (RS256, HS256, ES256)
typTipo do token
kidKey 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"]
  }
}
ClaimDescrição
issIssuer — quem emitiu o token
subSubject — ID único do usuário
audAudience — para quem o token é destinado
expExpiration — timestamp Unix de expiração
nbfNot Before — token inválido antes deste timestamp
iatIssued At — quando foi emitido
jtiJWT 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:

  1. Assinatura — verificar com a chave pública correta (kid no header → JWKS)
  2. iss — deve corresponder ao seu IdP
  3. aud — deve incluir o identificador da sua API
  4. exp — timestamp não pode ser passado (com tolerância de clock skew de ~60s)
  5. nbf — timestamp deve ser passado
  6. alg — nunca aceitar alg: "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 automaticamente

curl 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'

Recursos

On this page