Nivel 3 · 30 min
OAuth 2.0 + PKCE: Auth Bien Hecha
OAuth 2.0 es un framework de delegación: un usuario le da permiso a una app cliente para acceder a un recurso protegido sin compartir su contraseña. Bien hecho, es el patrón de autenticación más fuerte disponible para la mayoría de los equipos. Mal hecho, conseguís account takeovers, tokens filtrados y findings de auditoría. Authorization Code + PKCE es el único flow que deberías usar en 2026 para clientes nuevos.
El Authorization Code Flow
Pasos: (1) el cliente redirige el browser al servidor de autorización (ej: accounts.google.com) con response_type=code, client_id, redirect_uri, scope, state. (2) El usuario se autentica y consiente en el AS. (3) El AS redirige el browser de vuelta al redirect_uri con ?code=XYZ&state=.... (4) El servidor del cliente intercambia el code por tokens en el endpoint /token del AS, mandando el code, client_id, client_secret (para clientes confidenciales), y redirect_uri. (5) El AS devuelve access_token, refresh_token, id_token. Los flows deprecados: Implicit (response_type=token — el token viene en el fragmento de URL, fácil de filtrar) y Resource Owner Password Credentials (el cliente manda username/password directamente al AS — anula todo el sentido de OAuth). Los dos están removidos en OAuth 2.1.
PKCE: La Defensa Stateless para Clientes Públicos
Un cliente confidencial (backend de servidor web) tiene un client_secret que prueba identidad en el intercambio de tokens. Un cliente público (app mobile, SPA) no puede — todo lo que se shipea al browser/dispositivo es reverse-engineereable. Sin PKCE, un cliente público es vulnerable a interceptación del code de autorización: una app maliciosa en el mismo dispositivo registra el mismo custom URL scheme, intercepta la redirección con el auth code, y lo intercambia. PKCE (Proof Key for Code Exchange) lo arregla: (1) el cliente genera un code_verifier random de 43-128 chars, computa code_challenge = base64url(sha256(code_verifier)), y manda code_challenge + code_challenge_method=S256 con la auth request. (2) El AS guarda el challenge junto con el code emitido. (3) El cliente intercambia el code + el code_verifier original. (4) El AS verifica sha256(verifier) == challenge — solo el cliente original conoce el verifier, así que un interceptor que solo tiene el code no lo puede redimir. PKCE ahora es mandatorio para clientes públicos (RFC 7636) y recomendado para todos los clientes (OAuth 2.1).
state, nonce, redirect_uri y Almacenamiento de Tokens
state — un valor random por request que el cliente manda a /authorize y verifica al volver. Frena CSRF en el step de redirección (un atacante no puede iniciar un flow OAuth que aterrice en el redirect_uri de la víctima con el code del atacante, porque el state no matchearía). nonce (OIDC) — valor random mandado en la auth request, devuelto en el claim del id_token, previene replay. El redirect_uri debe estar registrado con match exacto en el AS — registrations open-redirect / wildcard / prefix-match han causado muchos account takeovers (el atacante hace que el AS redirija a evil.com/callback, cosechando el code). Almacenamiento de tokens: las SPAs deberían usar cookies HttpOnly Secure SameSite=Lax vía un BFF, no localStorage. Las apps mobile usan el OS Keychain (iOS) / Keystore (Android). Los refresh tokens DEBEN ser single-use con detección de rotación — el reuso señala compromiso. El documento BCP 212 (current best practice) es el checklist de seguridad de OAuth.
Code example
// SPA — generar par PKCE
const codeVerifier = base64UrlEncode(crypto.getRandomValues(new Uint8Array(32)));
const codeChallenge = base64UrlEncode(
await crypto.subtle.digest("SHA-256", new TextEncoder().encode(codeVerifier))
);
sessionStorage.setItem("pkce_verifier", codeVerifier);
const state = base64UrlEncode(crypto.getRandomValues(new Uint8Array(16)));
sessionStorage.setItem("oauth_state", state);
// Redirigir al AS
location.href = `https://auth.example.com/authorize?` +
`response_type=code` +
`&client_id=${CLIENT_ID}` +
`&redirect_uri=${encodeURIComponent(REDIRECT_URI)}` +
`&scope=openid%20profile` +
`&state=${state}` +
`&code_challenge=${codeChallenge}` +
`&code_challenge_method=S256`;
// Handler del callback — verificar state, intercambiar code+verifier por tokens
const params = new URLSearchParams(location.search);
if (params.get("state") !== sessionStorage.getItem("oauth_state")) {
throw new Error("state no coincide — posible CSRF");
}
const tokens = await fetch("https://auth.example.com/token", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "authorization_code",
code: params.get("code"),
redirect_uri: REDIRECT_URI,
client_id: CLIENT_ID,
code_verifier: sessionStorage.getItem("pkce_verifier")
})
}).then(r => r.json());