OAuth 2.1 e PKCE: fluxo de autorização moderno para SPAs e apps mobile
1. Por que OAuth 2.1? Principais mudanças em relação ao OAuth 2.0
O OAuth 2.1 consolida anos de experiência prática com o protocolo OAuth 2.0, eliminando fluxos inseguros e padronizando práticas que já eram recomendadas. As mudanças mais significativas incluem:
Eliminação do Implicit Grant — O fluxo implícito, que retornava tokens diretamente na URL de redirecionamento, foi removido por ser vulnerável a vazamento de tokens via histórico do navegador, logs de servidor e ataques de interceptação. Qualquer aplicação que antes usava Implicit Grant deve migrar para Authorization Code com PKCE.
Proibição do Resource Owner Password Credentials Grant — Este fluxo, que exigia que o usuário fornecesse suas credenciais diretamente ao cliente, foi removido por incentivar práticas inseguras e violar o princípio de delegação do OAuth. Aplicações legadas precisam adotar fluxos baseados em redirecionamento.
PKCE obrigatório para clients públicos — O Proof Key for Code Exchange, antes opcional, tornou-se requisito obrigatório para todos os clients que não podem armazenar um client secret com segurança (SPAs e apps mobile). Isso elimina a falsa sensação de segurança de usar um client secret embutido no código-fonte.
2. PKCE: o coração da segurança em clients públicos
O PKCE adiciona uma camada de proteção contra ataques de interceptação do código de autorização. O fluxo funciona com dois valores críticos:
Geração do code_verifier e code_challenge — O cliente gera uma string aleatória de 43 a 128 caracteres (o code_verifier). Em seguida, calcula o code_challenge usando o método S256 (SHA-256 hash + Base64URL) ou o método plain (apenas para ambientes restritos):
// Geração do code_verifier (43-128 caracteres alfanuméricos)
code_verifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
// Método S256: SHA-256 hash do code_verifier, codificado em Base64URL
code_challenge = base64url(sha256(code_verifier))
// Resultado: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"
Fluxo passo a passo — O cliente envia o code_challenge na requisição de autorização. O servidor de autorização o armazena temporariamente. Ao trocar o código pelos tokens, o cliente envia o code_verifier original. O servidor recalcula o hash e compara com o code_challenge armazenado. Se coincidirem, a troca é autorizada.
Por que PKCE substituiu o Client Secret — Em SPAs e apps mobile, o client secret está inevitavelmente exposto (no código JavaScript baixado pelo navegador ou no binário do app). O PKCE prova que o mesmo cliente que iniciou o fluxo está completando a troca, sem depender de segredos estáticos.
3. Fluxo completo do Authorization Code Grant com PKCE
Início da autorização — O cliente redireciona o usuário para o endpoint de autorização com os parâmetros obrigatórios:
GET /authorize?
response_type=code
&client_id=seu-client-id
&redirect_uri=https://seuapp.com/callback
&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
&code_challenge_method=S256
&state=xyz123protecaocsrf
&scope=openid%20profile%20email
Troca do código pelos tokens — Após o usuário autorizar, o servidor redireciona para a redirect_uri com o código de autorização. O cliente então faz uma requisição POST para o token endpoint:
POST /token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=oCodigoDeAutorizacao
&redirect_uri=https://seuapp.com/callback
&client_id=seu-client-id
&code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
Tratamento de erros comuns — O servidor pode retornar erros como:
- invalid_grant: código expirado (geralmente 10 minutos) ou já utilizado
- expired_code: o código ultrapassou o tempo de vida configurado
- mismatch: o code_verifier não corresponde ao code_challenge original
4. Implementação prática em SPAs
Armazenamento seguro do code_verifier — Nunca armazene o code_verifier em localStorage ou sessionStorage. Mantenha-o em memória (variável JavaScript) ou, se necessário, em um cookie HTTP-only com curta duração:
// Exemplo de fluxo completo em SPA
// 1. Gerar code_verifier e code_challenge
const generateCodeVerifier = () => {
const array = new Uint8Array(32);
crypto.getRandomValues(array);
return base64url(array);
};
const generateCodeChallenge = async (verifier) => {
const encoder = new TextEncoder();
const data = encoder.encode(verifier);
const digest = await crypto.subtle.digest('SHA-256', data);
return base64url(new Uint8Array(digest));
};
// 2. Iniciar autorização
const startAuth = async () => {
const codeVerifier = generateCodeVerifier();
sessionStorage.setItem('code_verifier', codeVerifier); // Apenas para exemplo
const codeChallenge = await generateCodeChallenge(codeVerifier);
const params = new URLSearchParams({
response_type: 'code',
client_id: 'seu-client-id',
redirect_uri: window.location.origin + '/callback',
code_challenge: codeChallenge,
code_challenge_method: 'S256',
state: crypto.randomUUID()
});
window.location.href = `https://auth.server.com/authorize?${params}`;
};
// 3. Trocar código por tokens
const exchangeCode = async (code) => {
const codeVerifier = sessionStorage.getItem('code_verifier');
sessionStorage.removeItem('code_verifier');
const response = await fetch('https://auth.server.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
code: code,
redirect_uri: window.location.origin + '/callback',
client_id: 'seu-client-id',
code_verifier: codeVerifier
})
});
return response.json(); // { access_token, refresh_token, id_token }
};
Proteção CSRF com state — O parâmetro state deve ser um valor único e imprevisível, armazenado em memória antes do redirecionamento. Ao receber o callback, compare o state retornado com o armazenado para evitar ataques de falsificação.
5. Implementação prática em Apps Mobile
iOS com ASWebAuthenticationSession — A Apple fornece uma API nativa que gerencia o fluxo de autenticação em uma sessão separada do navegador principal:
// Swift - iOS
import AuthenticationServices
let authURL = URL(string: "https://auth.server.com/authorize?response_type=code&client_id=seu-client-id&redirect_uri=meuapp://callback&code_challenge=...&code_challenge_method=S256&state=...")!
let session = ASWebAuthenticationSession(url: authURL, callbackURLScheme: "meuapp") { callbackURL, error in
guard let callbackURL = callbackURL else { return }
// Extrair o código de autorização da URL de callback
let components = URLComponents(url: callbackURL, resolvingAgainstBaseURL: false)
let code = components?.queryItems?.first(where: { $0.name == "code" })?.value
// Trocar código por tokens usando URLSession
}
session.prefersEphemeralWebBrowserSession = true
session.start()
Android com Chrome Custom Tabs — No Android, a abordagem recomendada é usar Custom Tabs com AppAuth:
// Kotlin - Android com AppAuth
import net.openid.appauth.AuthorizationRequest
import net.openid.appauth.AuthorizationService
val authRequest = AuthorizationRequest.Builder(
serviceConfiguration,
"seu-client-id",
ResponseTypeValues.CODE,
Uri.parse("meuapp://callback")
)
.setCodeVerifier(codeVerifier) // Gerado previamente
.setCodeVerifierChallenge(codeChallenge)
.setCodeVerifierChallengeMethod("S256")
.setState(state)
.build()
val authService = AuthorizationService(this)
val intent = authService.getAuthorizationRequestIntent(authRequest)
startActivityForResult(intent, AUTH_REQUEST_CODE)
Gerenciamento de deep links — Configure o app para receber callbacks via deep link (ex: meuapp://callback?code=...). No iOS, configure o URL scheme no Info.plist. No Android, adicione um intent filter no AndroidManifest.xml para capturar o URI personalizado.
6. Refresh Tokens e renovação de sessão sem fricção
Rotação de refresh tokens — Sempre que um refresh token é usado para obter novos access tokens, o servidor deve emitir um novo refresh token e invalidar o anterior. Isso limita o impacto de vazamentos:
POST /token
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token
&refresh_token=oRefreshTokenAtual
&client_id=seu-client-id
Detecção de reuso — Se um refresh token já rotacionado for usado novamente (indicando possível roubo), o servidor deve invalidar todos os tokens da sessão, forçando nova autenticação.
Estratégias para evitar logout indesejado — Em mobile, armazene o refresh token no Keychain (iOS) ou Keystore (Android). Em SPAs, use um cookie HTTP-only com flag Secure e SameSite=Strict. Configure o refresh token com expiração longa (dias ou semanas) enquanto o access token expira em minutos.
7. Boas práticas de segurança e armadilhas comuns
Validação de iss e aud no JWT — Ao decodificar o access token (se for JWT), sempre verifique:
- iss (issuer): deve corresponder ao domínio do servidor de autorização esperado
- aud (audience): deve conter o client_id da sua aplicação
Proteção contra interceptação de redirect URIs — Registre URIs exatas no servidor de autorização, sem curingas. Evite padrões como https://*.meusite.com/callback que permitem subdomínios maliciosos.
Mitigação de ataques comuns:
- Mix-up attack: sempre valide o issuer no token endpoint
- Downgrade attack: force o uso de HTTPS e rejeite conexões inseguras
- Token leakage: nunca logue tokens completos; registre apenas os últimos 4 caracteres para debugging
A adoção do OAuth 2.1 com PKCE representa o estado da arte em segurança para aplicações modernas. Ao eliminar fluxos inseguros e padronizar proteções como PKCE e rotação de refresh tokens, o protocolo oferece uma base sólida para autenticação e autorização em SPAs e apps mobile.
Referências
- The OAuth 2.1 Authorization Framework (IETF Draft) — Documento oficial do IETF que define o OAuth 2.1, incluindo todas as mudanças em relação ao OAuth 2.0 e a obrigatoriedade do PKCE.
- Proof Key for Code Exchange by OAuth Public Clients (RFC 7636) — Especificação original do PKCE, detalhando a geração do code_verifier, code_challenge e o fluxo completo de verificação.
- OAuth 2.0 for Browser-Based Applications (Best Current Practice) — Guia de melhores práticas para implementar OAuth em SPAs, incluindo recomendações de armazenamento e proteção contra ataques.
- AppAuth for Android — Biblioteca oficial do OpenID Foundation para implementar OAuth 2.0 e OpenID Connect em aplicativos Android, com suporte nativo a PKCE.
- AppAuth for iOS — Biblioteca oficial para iOS que integra com ASWebAuthenticationSession e gerencia todo o fluxo de autorização com PKCE.
- OAuth 2.1 and PKCE: Migrating from Implicit Grant (Auth0 Blog) — Artigo prático sobre como migrar aplicações existentes do Implicit Grant para Authorization Code com PKCE.
- OAuth 2.1: The New Standard for Secure Authorization (Okta Developer Blog) — Visão geral das mudanças do OAuth 2.1 com exemplos de código para implementação em diferentes plataformas.