O que é OAuth 2.0 e Por que Você Precisa Dele
OAuth 2.0 é um framework de autorização aberto que permite que aplicações acessem recursos em nome de um usuário sem nunca lidar com suas credenciais. Diferentemente da autenticação tradicional por usuário e senha, onde você entrega suas credenciais diretas à aplicação, OAuth 2.0 funciona como um intermediário confiável. Você autoriza a aplicação a usar seus dados em um provedor externo (Google, GitHub, etc.) sem compartilhar sua senha.
O contexto histórico é importante: antes de OAuth 2.0, as aplicações pediam que você fornecesse seu email e senha do Gmail, por exemplo, para acessar seus contatos. Isso era um pesadelo de segurança. OAuth 2.0 surgiu para resolver exatamente este problema. Hoje, quando você vê um botão "Entrar com Google", você está usando OAuth 2.0 por baixo dos panos. A diferença fundamental está em quem controla a sessão: no modelo antigo, a aplicação tinha sua senha; no OAuth, apenas o provedor de identidade a possui, e a aplicação recebe apenas um token de acesso limitado.
O padrão é amplamente adotado porque é simples, seguro quando implementado corretamente, e oferece um bom equilíbrio entre segurança e experiência do usuário. No entanto, OAuth 2.0 resolve apenas autorização, não autenticação. Você sabe que a aplicação pode acessar seus dados, mas não necessariamente que você é quem diz ser. Por isso surgiu o OpenID Connect.
Arquitetura e Atores Principais
Antes de mergulharmos nos flows, é essencial entender os atores envolvidos. Em qualquer cenário OAuth 2.0, você terá no mínimo quatro participantes principais: o Resource Owner (proprietário do recurso, geralmente o usuário), o Client (sua aplicação), o Authorization Server (o provedor que gerencia permissões) e o Resource Server (onde os dados reais estão armazenados). Frequentemente, o Authorization Server e o Resource Server são a mesma entidade fornecida pelo provedor (Google, Facebook, GitHub).
Fluxo de Comunicação Básico
O flow clássico funciona em três etapas principais. Primeiro, o usuário clica em um botão de login/autorização na sua aplicação. Segundo, a aplicação redireciona o usuário para o Authorization Server, que autentica o usuário e pede consentimento para compartilhar dados. Terceiro, após o consentimento, o Authorization Server redireciona o usuário de volta para sua aplicação com um código de autorização. Sua aplicação então troca este código por um token de acesso diretamente com o Authorization Server (sem envolver o navegador do usuário). Por fim, a aplicação usa este token para acessar os recursos do usuário.
┌─────────────┐ ┌──────────────────┐
│ Usuario │ │ Authorization │
│ (Browser) │ │ Server │
└──────┬──────┘ └────────┬─────────┘
│ │
│ 1. Clica "Login com Google" │
├─────────────────────────────────>│
│ │
│ 2. Autentica e pede permissão │
│<─────────────────────────────────┤
│ │
│ 3. Concede permissão │
├─────────────────────────────────>│
│ │
│ 4. Redireciona com código │
│<─────────────────────────────────┤
│
│ ┌─────────────┐
│ │ Sua │
│ │ Aplicação │
│ │ (Backend) │
│ └──────┬──────┘
│ │
│ 5. Passa código para backend
└──────────>│
│
│ 6. Troca código por token
│──────────────────────────>
│ (sem o usuário)
Entendendo Tokens
Um token em OAuth 2.0 é uma credencial que representa a autorização do usuário. Existem dois tipos principais: o access token e o refresh token. O access token é o que sua aplicação usa para fazer requisições em nome do usuário. É propositalmente de curta duração (minutos a horas) para minimizar o risco de exposição. O refresh token, por outro lado, tem uma duração muito mais longa e serve exclusivamente para obter um novo access token quando o atual expira.
Tokens são geralmente em formato JWT (JSON Web Token) ou opacos. Um JWT é um token auto-contido que contém informações codificadas sobre o usuário e permissões. Um token opaco é apenas uma string aleatória que o servidor usa para lookup de dados. JWTs são mais eficientes em cenários de múltiplos servidores porque não requerem chamadas de banco de dados para validação, mas são menos seguros se vazados porque contêm dados. Tokens opacos requerem um servidor centralizado para validação, mas são mais seguros porque são apenas referências.
OAuth 2.0: Flows em Detalhes
OAuth 2.0 define vários flows diferentes, cada um otimizado para um caso de uso específico. Escolher o flow correto é crítico para segurança e funcionalidade.
Authorization Code Flow
O Authorization Code Flow é o flow mais comum e seguro para aplicações web. É exatamente o que descrevi na seção anterior. A aplicação nunca vê a senha do usuário, e o token de acesso é obtido através de uma chamada backend-to-backend, que é muito mais segura do que passar tudo pelo navegador.
# Exemplo: Python com Flask e requests
from flask import Flask, request, redirect, session
import requests
import json
from urllib.parse import urlencode
app = Flask(__name__)
app.secret_key = 'sua-chave-secreta'
CLIENT_ID = 'seu-client-id'
CLIENT_SECRET = 'seu-client-secret'
REDIRECT_URI = 'http://localhost:5000/callback'
AUTHORIZATION_ENDPOINT = 'https://accounts.google.com/o/oauth2/v2/auth'
TOKEN_ENDPOINT = 'https://oauth2.googleapis.com/token'
USERINFO_ENDPOINT = 'https://www.googleapis.com/oauth2/v2/userinfo'
@app.route('/login')
def login():
# Etapa 1: Redirecionar o usuário para o Authorization Server
params = {
'client_id': CLIENT_ID,
'redirect_uri': REDIRECT_URI,
'response_type': 'code',
'scope': 'openid email profile',
'state': 'random-string-for-csrf-protection' # CSRF protection
}
authorization_url = f"{AUTHORIZATION_ENDPOINT}?{urlencode(params)}"
return redirect(authorization_url)
@app.route('/callback')
def callback():
# Etapa 2: O Authorization Server redireciona o usuário aqui com um código
code = request.args.get('code')
state = request.args.get('state')
if not code:
return 'Erro: nenhum código retornado', 400
# Etapa 3: Trocar o código por um token (backend-to-backend)
token_data = {
'code': code,
'client_id': CLIENT_ID,
'client_secret': CLIENT_SECRET,
'redirect_uri': REDIRECT_URI,
'grant_type': 'authorization_code'
}
response = requests.post(TOKEN_ENDPOINT, data=token_data)
if response.status_code != 200:
return f'Erro ao obter token: {response.text}', 400
token_response = response.json()
access_token = token_response.get('access_token')
# Etapa 4: Usar o token para acessar informações do usuário
headers = {'Authorization': f'Bearer {access_token}'}
user_response = requests.get(USERINFO_ENDPOINT, headers=headers)
user_info = user_response.json()
# Salvar na sessão
session['user'] = user_info
session['access_token'] = access_token
return redirect('/dashboard')
@app.route('/dashboard')
def dashboard():
user = session.get('user')
if not user:
return redirect('/login')
return f"Bem-vindo, {user.get('email')}"
if __name__ == '__main__':
app.run(debug=True)
Este exemplo implementa o flow completo. O ponto crítico aqui é que o CLIENT_SECRET nunca é exposto ao navegador. Ele fica seguro no backend. O navegador vê apenas o código de autorização, que é inútil sem o secret. Isso é por isso que este flow é o recomendado para aplicações web tradicionais.
Implicit Flow (Legado - NÃO USE)
O Implicit Flow era usado para Single Page Applications (SPAs) antes de técnicas modernas existirem. Neste flow, o token de acesso é retornado diretamente no URL do redirecionamento, sem usar um código de autorização intermediário. Isso era necessário porque SPAs rodam no navegador e não podem manter um backend secreto.
O problema é que tokens no URL são expostos na história do navegador, nos logs de servidor, e podem ser capturados por intermediários. O OAuth 2.0 Security Best Current Practice (BCP) explicitamente desencoraja este flow. Se você está construindo uma SPA moderna, use Authorization Code Flow com PKCE, que descrevo a seguir.
Authorization Code Flow com PKCE
PKCE (Proof Key for Code Exchange) foi criado para proteger aplicações públicas, como mobile apps e SPAs, que não podem manter um secret seguro. Funciona adicionando um desafio criptográfico ao flow.
// Exemplo: JavaScript/Node.js com Express
const express = require('express');
const axios = require('axios');
const crypto = require('crypto');
const session = require('express-session');
const app = express();
app.use(session({
secret: 'sua-chave-secreta',
resave: false,
saveUninitialized: true
}));
const CLIENT_ID = 'seu-client-id';
const REDIRECT_URI = 'http://localhost:3000/callback';
const AUTHORIZATION_ENDPOINT = 'https://github.com/login/oauth/authorize';
const TOKEN_ENDPOINT = 'https://github.com/login/oauth/access_token';
const USERINFO_ENDPOINT = 'https://api.github.com/user';
// Gerar PKCE challenge
function generatePKCE() {
const codeVerifier = crypto
.randomBytes(32)
.toString('base64')
.replace(/[+/=]/g, c => ({ '+': '-', '/': '_', '=': '' }[c]));
const codeChallenge = crypto
.createHash('sha256')
.update(codeVerifier)
.digest('base64')
.replace(/[+/=]/g, c => ({ '+': '-', '/': '_', '=': '' }[c]));
return { codeVerifier, codeChallenge };
}
app.get('/login', (req, res) => {
const { codeVerifier, codeChallenge } = generatePKCE();
// Armazenar o verifier na sessão para usar depois
req.session.codeVerifier = codeVerifier;
const params = new URLSearchParams({
client_id: CLIENT_ID,
redirect_uri: REDIRECT_URI,
response_type: 'code',
scope: 'user:email',
code_challenge: codeChallenge,
code_challenge_method: 'S256' // SHA256
});
res.redirect(`${AUTHORIZATION_ENDPOINT}?${params.toString()}`);
});
app.get('/callback', async (req, res) => {
const code = req.query.code;
const codeVerifier = req.session.codeVerifier;
if (!code || !codeVerifier) {
return res.status(400).send('Erro: código ou verifier ausente');
}
try {
// O Authorization Server valida que o hash do codeVerifier
// corresponde ao codeChallenge enviado anteriormente
const tokenResponse = await axios.post(TOKEN_ENDPOINT, {
client_id: CLIENT_ID,
code: code,
redirect_uri: REDIRECT_URI,
grant_type: 'authorization_code',
code_verifier: codeVerifier // Enviar o verifier original
}, {
headers: { 'Accept': 'application/json' }
});
const accessToken = tokenResponse.data.access_token;
// Obter informações do usuário
const userResponse = await axios.get(USERINFO_ENDPOINT, {
headers: { 'Authorization': `Bearer ${accessToken}` }
});
req.session.user = userResponse.data;
req.session.accessToken = accessToken;
res.redirect('/dashboard');
} catch (error) {
res.status(500).send(`Erro: ${error.message}`);
}
});
app.get('/dashboard', (req, res) => {
if (!req.session.user) {
return res.redirect('/login');
}
res.send(`Bem-vindo, ${req.session.user.login}`);
});
app.listen(3000, () => console.log('Servidor rodando na porta 3000'));
A mágica do PKCE está em dois valores: o code_verifier (uma string aleatória) e o code_challenge (o hash SHA256 do verifier). Você envia o challenge para o Authorization Server no primeiro passo, mas guarda o verifier. Quando troca o código por um token, você envia o verifier. O Authorization Server valida que o hash do verifier corresponde ao challenge enviado antes. Um atacante que interceptar o código não pode usar sem o verifier, porque não sabe qual string foi hasheada.
Client Credentials Flow
Este flow é para comunicação server-to-server, não envolvendo um usuário final. Quando sua aplicação precisa acessar um recurso que ela "possui" (não um recurso de um usuário), usa as credenciais da aplicação diretamente.
// Exemplo: Go com biblioteca stdlib
package main
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
)
const (
CLIENT_ID = "seu-client-id"
CLIENT_SECRET = "seu-client-secret"
TOKEN_ENDPOINT = "https://oauth.example.com/token"
API_ENDPOINT = "https://api.example.com/data"
)
type TokenResponse struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
ExpiresIn int `json:"expires_in"`
}
func getToken() (string, error) {
// Enviar credenciais diretamente (sem usuário envolvido)
data := url.Values{}
data.Set("grant_type", "client_credentials")
data.Set("client_id", CLIENT_ID)
data.Set("client_secret", CLIENT_SECRET)
data.Set("scope", "read:data")
resp, err := http.PostForm(TOKEN_ENDPOINT, data)
if err != nil {
return "", err
}
defer resp.Body.Close()
var tokenResp TokenResponse
json.NewDecoder(resp.Body).Decode(&tokenResp)
return tokenResp.AccessToken, nil
}
func accessProtectedResource(token string) error {
req, _ := http.NewRequest("GET", API_ENDPOINT, nil)
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
body, _ := ioutil.ReadAll(resp.Body)
fmt.Println(string(body))
return nil
}
func main() {
token, err := getToken()
if err != nil {
fmt.Println("Erro ao obter token:", err)
return
}
accessProtectedResource(token)
}
O Client Credentials Flow é direto: você envia seu CLIENT_ID e CLIENT_SECRET e recebe um token. Não há redirecionamento de navegador, não há consentimento do usuário. Use isto com cuidado, apenas para APIs que sua aplicação controla e em canais seguros (HTTPS).
OpenID Connect: Autenticação sobre Autorização
OpenID Connect (OIDC) é uma camada construída sobre OAuth 2.0 que adiciona autenticação. Enquanto OAuth 2.0 responde "A aplicação pode acessar estes recursos?", OIDC responde "Quem é este usuário?". Na prática, OIDC adiciona um novo token chamado ID Token ao fluxo.
ID Token vs Access Token
A diferença fundamental está no propósito. Um ID Token é um JWT que contém informações de identidade do usuário (quem ele é), como seu ID, email, nome. Ele é para a aplicação cliente, não para o resource server. Um Access Token, por outro lado, é para autorizar acessos a recursos e pode ser opaco. O ID Token nunca deve ser usado para acessar APIs; o access token é que faz isso.
// Exemplo de ID Token (JWT descodificado)
{
"iss": "https://accounts.google.com",
"azp": "seu-client-id.apps.googleusercontent.com",
"aud": "seu-client-id.apps.googleusercontent.com",
"sub": "110169547927130675053",
"email": "usuario@gmail.com",
"email_verified": true,
"iat": 1516239022,
"exp": 1516242622,
"nonce": "n-0S6_WzA2Mj"
}
O sub (subject) é o identificador único do usuário. O nonce é um valor arbitrário que você enviou no pedido de login; o servidor inclui no token para provar que o token foi realmente criado em resposta ao seu pedido (defesa contra certain attacks). O iat (issued at) e exp (expiration) controlam a validade do token.
Authorization Code Flow com OIDC
# Exemplo: Python com Flask-OIDC
from flask import Flask
from flask_oidc import FlaskOIDC
import os
app = Flask(__name__)
app.config['SECRET_KEY'] = 'seu-secret'
app.config['OIDC_CLIENT_SECRETS'] = 'client_secrets.json'
app.config['OIDC_ID_TOKEN_COOKIE_SECURE'] = False # True em produção
app.config['OIDC_REQUIRE_LOGIN'] = False
oidc = FlaskOIDC(app)
@app.route('/')
def index():
if oidc.user_loggedin:
return f'''
<h1>Bem-vindo, {oidc.user_getfield("email")}</h1>
<a href="/logout">Logout</a>
'''
else:
return '<a href="/login">Login com OIDC</a>'
@app.route('/login')
@oidc.require_login
def login():
return 'Autenticado!'
@app.route('/logout')
def logout():
oidc.logout()
return 'Desconectado'
if __name__ == '__main__':
app.run(debug=True)
E o arquivo client_secrets.json:
{
"web": {
"client_id": "seu-client-id.apps.googleusercontent.com",
"client_secret": "seu-client-secret",
"redirect_uris": ["http://localhost:5000/oidc/callback"],
"auth_uri": "https://accounts.google.com/o/oauth2/v2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"userinfo_uri": "https://www.googleapis.com/oauth2/v1/userinfo",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs"
}
}
A biblioteca Flask-OIDC cuida de toda a complexidade: extrai o ID Token, valida a assinatura, e coloca as informações do usuário acessíveis via oidc.user_getfield(). O flow é idêntico ao Authorization Code Flow, mas agora você recebe tanto um access token quanto um ID token.
Validando ID Tokens
Nunca confie cegamente em um token recebido do navegador. Um ID Token é um JWT, e JWTs podem ser falsificados se a assinatura não for validada. O servidor deve sempre verificar que:
- A assinatura é válida (usando a chave pública do Authorization Server)
- O
aud(audience) corresponde ao seu CLIENT_ID - O
iss(issuer) é o Authorization Server esperado - O token não está expirado (comparar
expcom a hora atual) - Se você enviou um
nonce, ele corresponde ao valor no token
// Exemplo: Validação de ID Token em Node.js com jwt-decode e node-fetch
const jwt = require('jsonwebtoken');
const fetch = require('node-fetch');
const ISSUER = 'https://accounts.google.com';
const CLIENT_ID = 'seu-client-id.apps.googleusercontent.com';
let publicKeys = null;
// Cache das chaves públicas do provedor
async function getPublicKeys() {
if (publicKeys) return publicKeys;
const response = await fetch(
'https://www.googleapis.com/oauth2/v1/certs'
);
const data = await response.json();
// Converter para formato que o jwt library espera
publicKeys = {};
for (const [kid, cert] of Object.entries(data)) {
publicKeys[kid] = cert;
}
return publicKeys;
}
async function validateIdToken(idTokenString) {
try {
// Decodificar sem validar para obter o header
const decoded = jwt.decode(idTokenString, { complete: true });
const kid = decoded.header.kid;
// Obter as chaves públicas
const keys = await getPublicKeys();
const publicKey = keys[kid];
if (!publicKey) {
throw new Error('Chave pública não encontrada');
}
// Validar a assinatura e claims
const verified = jwt.verify(idTokenString, publicKey, {
algorithms: ['RS256'],
audience: CLIENT_ID,
issuer: ISSUER
});
return verified;
} catch (error) {
console.error('Token inválido:', error.message);
return null;
}
}
// Uso
validateIdToken(idTokenFromBrowser)
.then(payload => {
if (payload) {
console.log('Usuário autenticado:', payload.email);
}
});
Este código baixa as chaves públicas do provedor de OIDC e usa a biblioteca jsonwebtoken para validar completamente o JWT. É um passo essencial que muitos desenvolvedores negligenciam.
Pitfalls Comuns e Como Evitá-los
1. Usar o Access Token como ID Token
O erro mais comum é pegar um access token e tentar extrair informações do usuário dele. Alguns provedores retornam tokens estruturados, e você pode ter sucesso em ambiente de desenvolvimento, mas isso não é seguro. Um access token é entre você e o resource server; pode mudar de formato ou ser revogado sem aviso. O ID Token é especificamente para identidade do usuário. Se você precisa de identidade, use OIDC e obtenha um ID Token.
2. Não Validar a Assinatura do Token
Confiar em um JWT sem validar a assinatura é como ler uma identidade falsa. Qualquer pessoa pode criar um JWT com qualquer payload. A validação da assinatura garante que o token foi realmente emitido pelo Authorization Server.
3. Armazenar Tokens em Local Storage
Em aplicações web, muitos desenvolvedores armazenam tokens em localStorage ou sessionStorage. Estes locais são vulneráveis a ataques XSS (Cross-Site Scripting). Se um atacante conseguir injetar JavaScript na página, ele pode acessar o token e usá-lo. A prática recomendada é armazenar em cookies HTTP-only, que não podem ser acessados por JavaScript.
// ERRADO - Vulnerável a XSS
localStorage.setItem('access_token', token);
// CORRETO - Servidor define cookie HTTP-only
// (No backend)
res.cookie('access_token', token, {
httpOnly: true,
secure: true, // HTTPS apenas
sameSite: 'Strict'
});
4. Não Usar HTTPS
OAuth 2.0 não é seguro sem HTTPS. O CLIENT_SECRET, tokens, e credentials são transmitidos em texto claro sem encriptação. Um atacante pode interceptar tudo em uma rede desprotegida. Sempre use HTTPS em produção. Sem exceções.
5. Implementar PKCE Incorretamente
PKCE é muitas vezes implementado com um code_challenge estático ou com um código_verifier que não é realmente aleatório. O desafio inteiro é que o atacante não pode adivinhar o verifier, então ele precisa ser criptograficamente aleatório. Use uma biblioteca criptográfica confiável.
6. Confiar no State Parameter Apenas Para CSRF
O state parameter é para proteção CSRF e para vincular o callback ao pedido original. Alguns desenvolvedores o ignoram em SPAs modernas pensando que CSRF não é um problema. CSRF ainda é um risco real. Sempre valide que o state retornado corresponde ao que você enviou. E não é suficiente para segurança de identidade; você ainda precisa validar o ID Token.
7. Não Implementar Refresh Token Rotation
Quando um usuário obtém um refresh token, se esse token vazar, um atacante pode obter novos access tokens indefinidamente. A prática segura é rotacionar refresh tokens: toda vez que é usado, o servidor emite um novo refresh token e invalida o antigo. Se um token antigo é usado, você detecta uma tentativa de replay.
# Exemplo de refresh token rotation
@app.route('/refresh-token', methods=['POST'])
def refresh_token():
old_refresh_token = request.json.get('refresh_token')
# Validar e usar o token antigo
user_id = validate_and_consume_refresh_token(old_refresh_token)
if not user_id:
return {'error': 'Refresh token inválido ou expirado'}, 401
# Emitir novos tokens
new_access_token = generate_access_token(user_id)
new_refresh_token = generate_refresh_token(user_id)
# O antigo é automaticamente inválido agora que foi "consumido"
return {
'access_token': new_access_token,
'refresh_token': new_refresh_token,
'token_type': 'Bearer',
'expires_in': 3600
}
8. Scope Creep
Quando um usuário autoriza sua aplicação, você pede um scope (conjunto de permissões). É tentador pedir scope=* ou todos os escopos disponíveis "para o futuro". Isso viola o princípio de menor privilégio. Peça apenas os escopos que você realmente precisa agora. Se precisar de mais depois, peça em um novo fluxo de consentimento incremental. Usuários confiam mais em aplicações que pedem permissões específicas e limitadas.
Conclusão
OAuth 2.0 e OpenID Connect são padrões fundamentais para a segurança moderna de aplicações, mas implementação incorreta é alarmantemente comum. Os pontos críticos a levar com você são:
Primeiro, escolha o flow correto para seu caso de uso. Authorization Code Flow para web apps tradicionais, Authorization Code com PKCE para SPAs e mobile apps, Client Credentials para server-to-server. Não use Implicit Flow em código novo.
Segundo, entenda a diferença entre OAuth 2.0 (autorização) e OpenID Connect (autenticação). Se você precisa saber quem é o usuário, use OIDC e valide o ID Token completamente. Não extraia identidade de access tokens.
Terceiro, segurança é um detalhe. HTTPS sempre, valide assinaturas sempre, use HTTP-only cookies, implemente PKCE corretamente, rotacione refresh tokens, e peça apenas os escopos necessários. Um token vazado ou mal validado pode comprometer toda a segurança de seus usuários.