Fundamentos de Programação/03 - Sistemas Reais/Semana 10 - Rede e HTTP8 min
OAuth
- OAuth é autenticação ou autorização? E onde OpenID Connect entra?
- Quem são os quatro papéis: resource owner, client, auth server, resource server?
- Por que o authorization code flow existe se o implicit era mais simples?
- O que o PKCE protege em aplicações que não podem guardar segredo?
Conceito
OAuth 2.0 é um framework de autorização delegada, não de autenticação.
Ele responde "este aplicativo pode acessar os dados daquele usuário naquele serviço?" — não "quem é este usuário?".
Quem responde a segunda pergunta é o OpenID Connect (OIDC), uma camada sobre OAuth 2.0
que adiciona o id_token (um JWT com claims sobre a identidade) e o endpoint userinfo. Usar
OAuth puro para login é o erro conceitual mais comum da área — e a origem de vulnerabilidades
reais, porque um access_token prova acesso, não identidade.
Os quatro papéis — nomeá-los corretamente resolve metade da confusão:
| Papel | Quem é | No exemplo "entrar com Google" |
|---|---|---|
| resource owner | o usuário | você |
| client | o aplicativo que quer acesso | o site que você está usando |
| authorization server | quem autentica e emite tokens | o Google (accounts.google.com) |
| resource server | quem guarda os dados protegidos | a API do Google |
Por que o authorization code flow existe, se o implicit era mais simples: o implicit
devolvia o token direto na URL (no fragmento). URLs vão para histórico, log de proxy e
header Referer (6. Headers HTTP) — o token vazava por vários caminhos, e não havia como
autenticar o cliente na entrega.
O code flow devolve um código de uso único e vida curta na URL, e o cliente troca esse código pelo token num canal de trás (servidor a servidor, POST). O token nunca passa pela barra de endereço. Por isso o implicit foi deprecado no OAuth 2.1.
O que o PKCE protege: clientes que não conseguem guardar segredo — SPA e mobile, onde qualquer "client secret" embutido é público. Sem segredo, um atacante que interceptasse o código (via app malicioso registrando o mesmo custom scheme, ou log) poderia trocá-lo pelo token.
PKCE (Proof Key for Code Exchange) resolve com um segredo por requisição:
- o cliente gera um
code_verifieraleatório - envia
code_challenge = BASE64URL(SHA256(verifier))no pedido de autorização - ao trocar o código, envia o
verifieroriginal - o servidor confere
SHA256(verifier) == challengeguardado
Quem roubou só o código não tem o verifier, e o código é inútil. Hoje PKCE é recomendado
para todos os clientes, incluindo os confidenciais.
Scope e consent: o scope é o que o cliente pede permissão para fazer
(read:pedidos); o consent é a tela onde o usuário concorda. O princípio é o menor
privilégio: peça o mínimo, porque a tela de consentimento é onde o usuário desiste.
Em Go
A biblioteca é golang.org/x/oauth2 (semi-oficial, mantida pelo time de Go), e o fluxo
tem três passos:
conf := &oauth2.Config{
ClientID: os.Getenv("CLIENT_ID"),
ClientSecret: os.Getenv("CLIENT_SECRET"),
RedirectURL: "https://meuapp.com/callback",
Scopes: []string{"read:pedidos"}, // o MÍNIMO necessário
Endpoint: oauth2.Endpoint{
AuthURL: "https://provedor/authorize",
TokenURL: "https://provedor/token",
},
}
// 1. redireciona o usuário, com state (CSRF) e PKCE
verifier := oauth2.GenerateVerifier()
url := conf.AuthCodeURL(state, oauth2.AccessTypeOffline, oauth2.S256ChallengeOption(verifier))
// 2. no callback: confira o state ANTES de qualquer coisa
if r.URL.Query().Get("state") != stateGuardadoNaSessao {
http.Error(w, "state inválido", http.StatusBadRequest); return
}
// 3. troca o código pelo token, no canal de trás
tok, err := conf.Exchange(ctx, r.URL.Query().Get("code"), oauth2.VerifierOption(verifier))
// 4. cliente que renova o token automaticamente
cli := conf.Client(ctx, tok) // usa o refresh_token quando o access expira
O parâmetro state não é opcional. Ele é a defesa contra CSRF no fluxo: sem ele, um
atacante induz seu usuário a completar um callback com o código do atacante, ligando a
conta da vítima à conta dele. Guarde o state na sessão (ou num cookie assinado) e compare
antes de trocar o código.
conf.Client(ctx, tok) devolve um *http.Client que renova o token sozinho usando o
refresh_token. Ele é a razão de a integração ficar curta — e note que ele não tem
timeout configurado por padrão, então envolva com o seu Transport
(2. Ciclo de uma Requisição).
Para OIDC (login), a biblioteca é github.com/coreos/go-oidc. Ela faz o que x/oauth2
não faz: descobre a configuração do provedor, busca as chaves públicas (JWKS) e
verifica o id_token.
id_token sem verificarVerificar significa: assinatura contra a JWKS do provedor, iss (emissor esperado), aud
(o seu client ID), exp, e o nonce que você enviou. Faltando qualquer um, o token pode
ter sido emitido para outro aplicativo e reaproveitado no seu.
É o mesmo princípio de 7. Sessão vs JWT: nunca confie no que o token diz antes de validar como ele foi assinado.
Do lado servidor, se você é o resource server: valide o access_token (via JWKS se for
JWT, ou via introspection se for opaco) e verifique o scope antes de autorizar — token
válido não significa permissão para aquela operação (5. Status Codes: 401 contra 403).
Respostas às perguntas-guia
1. OAuth é autenticação ou autorização? E onde OpenID Connect entra?
Autorização delegada. OIDC é a camada sobre OAuth 2.0 que adiciona autenticação, com
id_token e userinfo. Usar access_token como prova de identidade é o erro clássico.
Em Go: x/oauth2 faz OAuth; go-oidc faz OIDC (incluindo a verificação do id_token).
2. Quem são os quatro papéis: resource owner, client, auth server, resource server?
Usuário; aplicativo; quem emite tokens; quem guarda os dados. A tabela acima.
3. Por que o authorization code flow existe se o implicit era mais simples?
Porque o implicit devolvia o token na URL — vazando por histórico, log e Referer, sem
autenticar o cliente. O code flow entrega um código de uso único e faz a troca pelo token num
canal de trás. Implicit está deprecado no OAuth 2.1.
4. O que o PKCE protege em aplicações que não podem guardar segredo?
Protege a troca do código: quem interceptar o código não tem o code_verifier, então não
consegue obter o token. Hoje é recomendado para todos os clientes.
Em Go: oauth2.GenerateVerifier(), oauth2.S256ChallengeOption(v) no AuthCodeURL e
oauth2.VerifierOption(v) no Exchange — três chamadas.
Trade-offs
Do conceito:
- Delegar autenticação a um provedor compra menos senha para guardar e cobra dependência de disponibilidade e política de terceiros.
- Code flow + PKCE compra segurança e cobra um passo de servidor (o callback).
- Scope granular compra menor privilégio e cobra tela de consentimento mais assustadora.
access_tokende vida curta + refresh compra janela pequena e cobra estado (7. Sessão vs JWT).
Em Go:
x/oauth2compra o fluxo pronto e cobra que você implemente ostatecorretamente.conf.Clientcompra renovação automática e esconde quando o refresh acontece (e falha).go-oidccompra verificação correta deid_token; escrever isso à mão é onde nascem as vulnerabilidades.
Exemplo prático
O fluxo inteiro, com o state e o PKCE calculados só com a stdlib — para ver o que as
bibliotecas fazem por baixo:
package main
import (
"crypto/rand"
"crypto/sha256"
"crypto/subtle"
"encoding/base64"
"fmt"
"net/url"
"strings"
)
func aleatorio(n int) string {
b := make([]byte, n)
rand.Read(b) // crypto/rand
return base64.RawURLEncoding.EncodeToString(b)
}
// PKCE: challenge = BASE64URL(SHA256(verifier))
func desafio(verifier string) string {
s := sha256.Sum256([]byte(verifier))
return base64.RawURLEncoding.EncodeToString(s[:])
}
// --- o que o authorization server guarda entre os dois passos ---
type pedidoAutorizacao struct {
challenge string
clienteID string
usado bool
}
type authServer struct{ codigos map[string]*pedidoAutorizacao }
func (a *authServer) autoriza(clienteID, challenge string) string {
code := aleatorio(16)
a.codigos[code] = &pedidoAutorizacao{challenge: challenge, clienteID: clienteID}
return code
}
func (a *authServer) troca(code, verifier string) (string, error) {
p, ok := a.codigos[code]
if !ok {
return "", fmt.Errorf("código desconhecido")
}
if p.usado {
return "", fmt.Errorf("código já usado (uso único)")
}
// A VERIFICAÇÃO DO PKCE: quem tem o código sem o verifier não passa
if subtle.ConstantTimeCompare([]byte(desafio(verifier)), []byte(p.challenge)) != 1 {
return "", fmt.Errorf("code_verifier não corresponde ao challenge")
}
p.usado = true
return "access_token_" + aleatorio(8), nil
}
func main() {
as := &authServer{codigos: map[string]*pedidoAutorizacao{}}
// ===== PASSO 1: o cliente prepara state e PKCE =====
state := aleatorio(16) // defesa contra CSRF no callback
verifier := aleatorio(32) // segredo POR REQUISIÇÃO
challenge := desafio(verifier)
u, _ := url.Parse("https://provedor/authorize")
q := u.Query()
q.Set("response_type", "code")
q.Set("client_id", "meu-app")
q.Set("redirect_uri", "https://meuapp.com/callback")
q.Set("scope", "read:pedidos") // o MÍNIMO
q.Set("state", state)
q.Set("code_challenge", challenge)
q.Set("code_challenge_method", "S256")
u.RawQuery = q.Encode()
fmt.Println("1. redireciona o usuário para:")
fmt.Println(" " + strings.ReplaceAll(u.String(), "&", "\n &"))
// ===== PASSO 2: usuário autentica e consente; volta com o CÓDIGO =====
code := as.autoriza("meu-app", challenge)
stateVolta := state
fmt.Printf("\n2. callback recebe: code=%s state=%s\n", code[:8]+"...", stateVolta[:8]+"...")
// ===== PASSO 3: confira o state ANTES de qualquer coisa =====
if subtle.ConstantTimeCompare([]byte(stateVolta), []byte(state)) != 1 {
fmt.Println(" state inválido: possível CSRF, aborta")
return
}
fmt.Println(" state confere")
// ===== PASSO 4: troca no CANAL DE TRÁS, com o verifier =====
tok, err := as.troca(code, verifier)
fmt.Printf("\n3. troca com o verifier correto: token=%s err=%v\n", tok, err)
// ===== Os ataques que o desenho impede =====
fmt.Println("\n--- o que o desenho barra ---")
code2 := as.autoriza("meu-app", challenge)
_, err = as.troca(code2, aleatorio(32)) // atacante roubou o código, não tem o verifier
fmt.Printf("código roubado, verifier errado: %v\n", err)
code3 := as.autoriza("meu-app", challenge)
as.troca(code3, verifier)
_, err = as.troca(code3, verifier) // replay do mesmo código
fmt.Printf("mesmo código usado duas vezes: %v\n", err)
// state diferente = callback induzido por atacante
if subtle.ConstantTimeCompare([]byte(aleatorio(16)), []byte(state)) != 1 {
fmt.Println("state de outra origem: rejeitado antes da troca")
}
}
A linha código roubado, verifier errado é o PKCE inteiro: o código na URL deixa de ser
suficiente. E mesmo código usado duas vezes é o uso único, que limita o dano de um replay.
Relacionado
- 7. Sessão vs JWT — o que fazer com o token depois de recebê-lo
- 6. Headers HTTP — por que o implicit vazava pela URL e pelo
Referer - 5. Status Codes — token inválido é 401; escopo insuficiente é 403
- 3. TLS — todo o fluxo pressupõe canal autenticado
Parte de Semana 10 - Rede e HTTP · 00 - MOC Fundamentos de Programação