Los flujos de OAuth 2.0 sin misterios
Authorization Code, PKCE, Client Credentials — qué flujo de OAuth usar según el tipo de aplicación y qué fallos de seguridad se esconden en cada uno.

OAuth 2.0 es el estándar para la autorización delegada, pero su especificación es tan flexible que resulta confusa. La variedad de grant types, parámetros opcionales y extensiones propias de cada proveedor dificulta saber qué flujo es el correcto para tu aplicación. La mayoría de las vulnerabilidades de seguridad en implementaciones de OAuth surgen de elegir el flujo equivocado o de saltarse pasos de validación críticos.
Los cuatro flujos que importan
OAuth 2.0 define varios grant types. En la práctica, cuatro de ellos cubren todos los casos de uso modernos.
| Flujo | Caso de uso | Tipo de cliente | ¿Involucra al usuario? |
|---|---|---|---|
| Authorization Code + PKCE | Aplicaciones web, aplicaciones móviles, SPA | Público | Sí |
| Client Credentials | Servicio a servicio | Confidencial | No |
| Device Authorization | Smart TVs, herramientas CLI | Público | Sí |
| Refresh Token | Extensión de la duración de la sesión | Ambos | No (tras la autenticación) |
El flujo Implicit y el flujo Resource Owner Password están obsoletos. Si usas alguno de los dos, migra ahora.
Authorization Code con PKCE
Este es el flujo recomendado para cualquier aplicación en la que un usuario inicie sesión. PKCE (Proof Key for Code Exchange) evita los ataques de interceptación del código de autorización.
// Step 1: Generate PKCE challenge
import crypto from "crypto";
function generatePKCE() {
const verifier = crypto.randomBytes(32).toString("base64url");
const challenge = crypto
.createHash("sha256")
.update(verifier)
.digest("base64url");
return { verifier, challenge };
}
// Step 2: Redirect user to authorization server
function getAuthorizationUrl(pkce: { challenge: string }) {
const params = new URLSearchParams({
response_type: "code",
client_id: process.env.OAUTH_CLIENT_ID!,
redirect_uri: "https://myapp.com/callback",
scope: "openid profile email",
state: crypto.randomBytes(16).toString("hex"),
code_challenge: pkce.challenge,
code_challenge_method: "S256",
});
return `https://auth.provider.com/authorize?${params}`;
}// Step 3: Exchange authorization code for tokens
async function exchangeCode(code: string, verifier: string) {
const response = await fetch("https://auth.provider.com/token", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "authorization_code",
code,
redirect_uri: "https://myapp.com/callback",
client_id: process.env.OAUTH_CLIENT_ID!,
code_verifier: verifier, // PKCE verification
}),
});
return response.json();
// { access_token: "...", refresh_token: "...", id_token: "...", expires_in: 3600 }
}El parámetro state evita los ataques CSRF. El code_verifier demuestra que el mismo cliente que inició el flujo es el que lo está completando.
Flujo Client Credentials
Pensado para la comunicación servicio a servicio, donde no interviene ningún usuario. El cliente se autentica con sus propias credenciales.
// ✅ Service-to-service authentication
async function getServiceToken() {
const response = await fetch("https://auth.provider.com/token", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Authorization: `Basic ${Buffer.from(
`${process.env.CLIENT_ID}:${process.env.CLIENT_SECRET}`,
).toString("base64")}`,
},
body: new URLSearchParams({
grant_type: "client_credentials",
scope: "api:read api:write",
}),
});
return response.json();
}
// Cache the token until near expiry
let cachedToken: { token: string; expiresAt: number } | null = null;
async function getValidToken(): Promise<string> {
if (cachedToken && cachedToken.expiresAt > Date.now() + 60000) {
return cachedToken.token;
}
const data = await getServiceToken();
cachedToken = {
token: data.access_token,
expiresAt: Date.now() + data.expires_in * 1000,
};
return cachedToken.token;
}Nunca expongas los client secrets en código frontend. Client Credentials es exclusivamente para aplicaciones del lado del servidor.
Errores de seguridad comunes
// ❌ Not validating the state parameter — CSRF vulnerability
app.get("/callback", async (req, res) => {
const { code } = req.query;
const tokens = await exchangeCode(code); // Missing state check!
});
// ✅ Always validate state
app.get("/callback", async (req, res) => {
const { code, state } = req.query;
const savedState = req.session.oauthState;
if (!state || state !== savedState) {
return res.status(403).json({ error: "Invalid state parameter" });
}
delete req.session.oauthState;
const tokens = await exchangeCode(code, req.session.pkceVerifier);
});// ❌ Not validating the ID token
const user = jwt.decode(tokens.id_token); // Decode without verify!
// ✅ Verify the ID token signature, issuer, and audience
const user = jwt.verify(tokens.id_token, publicKey, {
issuer: "https://auth.provider.com",
audience: process.env.OAUTH_CLIENT_ID,
algorithms: ["RS256"],
});Almacenamiento y ciclo de vida de los tokens
// Access token: short-lived, used for API calls
// Refresh token: long-lived, used to get new access tokens
async function refreshAccessToken(refreshToken: string) {
const response = await fetch("https://auth.provider.com/token", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "refresh_token",
refresh_token: refreshToken,
client_id: process.env.OAUTH_CLIENT_ID!,
}),
});
if (!response.ok) {
// Refresh token expired or revoked — user must re-authenticate
throw new AuthenticationError("Session expired");
}
return response.json();
}| Token | Duración | Almacenamiento | Estrategia de renovación |
|---|---|---|---|
| access token | 15-60 minutos | Memoria o cookie HTTP-only | Usar el refresh token |
| refresh token | 7-30 días | Cookie HTTP-only o lado del servidor seguro | Reautenticar al expirar |
| ID token | Igual que el access token | Memoria | No se renueva — se vuelve a obtener al renovar el access token |
Puntos clave
- Usa Authorization Code + PKCE para cualquier aplicación de cara al usuario: web, móvil o SPA
- Client Credentials es solo para comunicación servicio a servicio — nunca expongas client secrets en el navegador
- Valida siempre el parámetro
state— omitirlo abre una vulnerabilidad CSRF - Verifica las firmas del ID token — decodificar sin verificar confía en datos sin firmar
- Los access tokens de corta duración junto con la rotación de refresh tokens limitan la ventana de daño de un token robado
- El flujo Implicit está obsoleto — migra a Authorization Code + PKCE


