trilha

Fundamentos de Programação/03 - Sistemas Reais/Semana 10 - Rede e HTTP8 min

OAuth

Perguntas-guia
  • 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:

  1. o cliente gera um code_verifier aleatório
  2. envia code_challenge = BASE64URL(SHA256(verifier)) no pedido de autorização
  3. ao trocar o código, envia o verifier original
  4. o servidor confere SHA256(verifier) == challenge guardado

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.

Nunca confie num id_token sem verificar

Verificar 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_token de vida curta + refresh compra janela pequena e cobra estado (7. Sessão vs JWT).

Em Go:

  • x/oauth2 compra o fluxo pronto e cobra que você implemente o state corretamente.
  • conf.Client compra renovação automática e esconde quando o refresh acontece (e falha).
  • go-oidc compra verificação correta de id_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


Parte de Semana 10 - Rede e HTTP · 00 - MOC Fundamentos de Programação

Buscar

Busca por título, seção e texto das notas