Fase 5 6 horas Avançado

A Grande Corrida

Na reta final, só passa quem tem credencial. OAuth 2.0, OIDC e JWT são os portões da segurança digital — saber abri-los é chegar primeiro.

Fase 5 — A Grande Corrida · OAuth 2.0, OIDC e JWT
Progresso: 0/8 (0%)

Objetivos da Fase

O que você vai aprender:

  • OAuth 2.0: fluxo de autorização passo a passo
  • OIDC (OpenID Connect): identidade sobre OAuth
  • Estrutura de um JWT (header.payload.signature)
  • Scopes, claims e o princípio do menor privilégio
  • Diferença entre autenticação e autorização
  • Provedores OIDC: Google, GitHub, Microsoft
  • Biblioteca PyJWT para JWT em Python

O que você vai criar:

  • Decodificador de JWT manual (base64url)
  • Simulador do fluxo OAuth 2.0
  • Validador de token OIDC com PyJWT
  • Dashboard HTTP com rota protegida

🏰 Convite ao Reino OIDC

Para reforçar OAuth 2.0, OIDC e JWT com uma narrativa complementar, visite o Reino OIDC antes de seguir para o projeto da fase.

Entrar no Reino OIDC

5.1 — O Portão da Corrida: OAuth 2.0

Imagine a largada: o fiscal pede sua credencial, você apresenta sua identidade, ele emite um crachá temporário. Esse crachá é o access token.

🔑 Fluxo Authorization Code

  1. Usuário clica em "Entrar com Google"
  2. App gera um code_verifier secreto e envia só o code_challenge (S256): assim um código interceptado não pode ser trocado por outra pessoa
  3. App envia um nonce aleatório: ele precisa voltar dentro do ID Token para impedir que um token antigo seja reapresentado
  4. Usuário autentica e autoriza
  5. Provedor retorna um authorization_code
  6. App troca o código enviando o code_verifier e recebe access_token + id_token
  7. App confere o nonce do ID Token com o que enviou antes de usar a sessão
# Simulador do fluxo OAuth 2.0 em Python
import base64
import hashlib
import secrets
import urllib.parse

CLIENT_ID = "circuito_ferradura_app"
REDIRECT_URI = "http://localhost:8080/callback"
SCOPE = "openid profile email"

code_verifier = secrets.token_urlsafe(64)
code_challenge = base64.urlsafe_b64encode(
    hashlib.sha256(code_verifier.encode()).digest()
).rstrip(b"=").decode()
nonce = secrets.token_urlsafe(16)
state = secrets.token_urlsafe(16)

params = {
    "response_type": "code",
    "client_id": CLIENT_ID,
    "redirect_uri": REDIRECT_URI,
    "scope": SCOPE,
    "state": state,
    "code_challenge": code_challenge,
    "code_challenge_method": "S256",
    "nonce": nonce,
}
import json
import urllib.request
descoberta = json.load(urllib.request.urlopen(
    "https://accounts.google.com/.well-known/openid-configuration"
))
authorization_endpoint = descoberta["authorization_endpoint"]
url = authorization_endpoint + "?" + urllib.parse.urlencode(params)
print(f"→ Redirecionar para:\n{url}")

troca = {
    "grant_type": "authorization_code",
    "code": authorization_code,
    "redirect_uri": REDIRECT_URI,
    "client_id": CLIENT_ID,
    "code_verifier": code_verifier,
}

5.2 — O Número do Peito: JWT

Cada piloto tem um número no peito — único e verificável. O JWT funciona assim: três partes separadas por ponto, codificadas em base64url.

# Estrutura de um JWT
# HEADER.PAYLOAD.SIGNATURE
#
# Header  → {"alg": "RS256", "typ": "JWT"}
# Payload → {"sub": "12345", "name": "Ciclano",
#             "email": "ci@ferradura.dev", "iat": 1702930800}

import base64
import json

def decodificar_jwt(token: str) -> dict:
    partes = token.split(".")

    def decode_b64(valor: str) -> dict:
        padding = "=" * (-len(valor) % 4)
        dados = base64.urlsafe_b64decode(valor + padding)
        return json.loads(dados)

    header = decode_b64(partes[0])
    payload = decode_b64(partes[1])

    return {
        "header": header,
        "payload": payload,
        "assinatura": partes[2][:20] + "...",
    }

# Exemplo com PyJWT (validação com a chave publicada)
import jwt
jwks = jwt.PyJWKClient(jwks_uri)
key = jwks.get_signing_key_from_jwt(token).key
claims = jwt.decode(
    token,
    key,
    algorithms=["RS256"],
    audience=CLIENT_ID,
    issuer=ISSUER,
)
if claims.get("nonce") != nonce_enviado:
    raise ValueError("nonce não confere, recusado")
print(f"Bem-vindo, {claims['name']}!")

5.3 — OIDC: A Identidade sobre o OAuth

OAuth diz "você pode acessar". OIDC responde "mas quem é você?". O id_token contém as claims de identidade verificáveis.

# Claims típicas de um id_token OIDC
import time

ISS_ESPERADO = "https://accounts.google.com"
nonce_enviado = nonce

id_token_payload = {
    "iss": ISS_ESPERADO,
    "sub": "103456789012345678901",
    "aud": CLIENT_ID,
    "exp": int(time.time()) + 300,
    "iat": int(time.time()),
    "nonce": nonce_enviado,
    "name": "Ciclano da Corrida",
    "email": "ciclano@ferradura.dev",
}

def piloto_autenticado(claims: dict, assinatura_valida: bool) -> bool:
    # assinatura conferida com a chave publicada em jwks_uri
    if not assinatura_valida:
        raise ValueError("assinatura inválida: token recusado")
    if claims.get("iss") != ISS_ESPERADO:
        raise ValueError("iss errado: token de outro sistema, recusado")
    if claims.get("aud") != CLIENT_ID:
        raise ValueError("aud errado: token emitido para outra aplicação, recusado")
    if claims.get("exp", 0) <= time.time():
        raise ValueError("token expirado, recusado")
    if claims.get("nonce") != nonce_enviado:
        raise ValueError("nonce não confere, recusado")
    return True

# Token correto: aceito. email_verified não autentica ninguém.
piloto_autenticado(id_token_payload, assinatura_valida=True)

# Exemplo de token recusado: iss de outro sistema
token_recusado = dict(id_token_payload, iss="https://outro.example")
try:
    piloto_autenticado(token_recusado, assinatura_valida=True)
except ValueError as erro:
    print(erro)

✅ Checklist da Fase 5

🔐 Projeto da Fase 5

Crie um micro-servidor Flask com duas rotas: /login (redireciona para Google OAuth) e /dashboard (protegida — exige id_token válido na sessão). Use PyJWT para validar o token e exiba o nome do piloto autenticado.

🔒 LGPD — Usamos localStorage para salvar progresso.