En las aplicaciones web y moviles modernas, el login social como “Iniciar sesion con Google” o “Iniciar sesion con GitHub” se ha vuelto comun. Detras de esto funcionan dos especificaciones estandar: OAuth 2.0 y OpenID Connect (OIDC). En este articulo, explicamos en detalle estos mecanismos desde lo basico hasta lo avanzado.
Diferencia entre OAuth 2.0 y OIDC
Primero, aclaremos la diferencia entre OAuth 2.0 y OIDC, que a menudo se confunden.
| Elemento | OAuth 2.0 | OpenID Connect |
|---|---|---|
| Proposito | Autorizacion (Authorization) | Autenticacion (Authentication) |
| Pregunta que responde | ”Permite a esta app acceder a los recursos?" | "Quien eres tu?” |
| Que se obtiene | Access Token | ID Token + Access Token |
| Ano de especificacion | 2012 (RFC 6749) | 2014 |
Diferencia entre autenticacion y autorizacion:
- Autenticacion (Authentication): Proceso para confirmar “quien es” el usuario
- Autorizacion (Authorization): Proceso para determinar “a que puede acceder” el usuario
Actores de OAuth 2.0
En OAuth 2.0 existen 4 roles principales.
flowchart TB
RO["Resource Owner<br/>(Propietario del recurso/Usuario)<br/>El dueno de los datos. Normalmente el usuario final."]
AS["Authorization Server<br/>(Servidor de autorizacion)<br/>Emite tokens. Google, GitHub, etc."]
Client["Client<br/>(Cliente)<br/>La aplicacion que quiere acceder a los recursos."]
RS["Resource Server<br/>(Servidor de recursos)<br/>Aloja los recursos protegidos. APIs, etc."]
RO -->|Otorga autorizacion| AS
AS -->|Emite token| Client
Client -->|Solicitud API| RS
Tipos de Grant en OAuth 2.0
OAuth 2.0 define multiples “tipos de grant” (flujos) para diferentes casos de uso.
1. Authorization Code Grant (Grant de codigo de autorizacion)
Es el flujo mas seguro y recomendado. Se usa en aplicaciones del lado del servidor.
sequenceDiagram
participant U as User
participant C as Client
participant AS as Auth Server
participant RS as Resource Server
U->>C: 1. Iniciar login
C->>AS: 2. Solicitud de autorizacion
AS->>U: 3. Pantalla de login y consentimiento
U->>AS: 4. Autenticacion y consentimiento
AS->>C: 5. Codigo de autorizacion
C->>AS: 6. Intercambio de token
AS->>C: 7. Access Token
C->>RS: 8. Llamada API
RS->>C: 9. Recurso
// Step 2: Generacion de URL de solicitud de autorizacion
const authorizationUrl = new URL('https://auth.example.com/authorize');
authorizationUrl.searchParams.set('response_type', 'code');
authorizationUrl.searchParams.set('client_id', CLIENT_ID);
authorizationUrl.searchParams.set('redirect_uri', REDIRECT_URI);
authorizationUrl.searchParams.set('scope', 'openid profile email');
authorizationUrl.searchParams.set('state', generateRandomState());
// Step 6: Intercambio de token
const tokenResponse = 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: authorizationCode,
redirect_uri: REDIRECT_URI,
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
}),
});
2. Authorization Code Grant con PKCE
Extension para SPAs y apps moviles donde el client secret no puede almacenarse de forma segura.
// PKCE: Proof Key for Code Exchange
import crypto from 'crypto';
// Step 1: Generacion de Code Verifier (cadena aleatoria de 43-128 caracteres)
const codeVerifier = crypto.randomBytes(32)
.toString('base64url');
// Step 2: Generacion de Code Challenge
const codeChallenge = crypto
.createHash('sha256')
.update(codeVerifier)
.digest('base64url');
// Step 3: Agregar code_challenge a la solicitud de autorizacion
const authUrl = new URL('https://auth.example.com/authorize');
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('client_id', CLIENT_ID);
authUrl.searchParams.set('redirect_uri', REDIRECT_URI);
authUrl.searchParams.set('scope', 'openid profile');
authUrl.searchParams.set('code_challenge', codeChallenge);
authUrl.searchParams.set('code_challenge_method', 'S256');
authUrl.searchParams.set('state', generateState());
// Step 4: Enviar code_verifier en el intercambio de token
const tokenResponse = 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: authorizationCode,
redirect_uri: REDIRECT_URI,
client_id: CLIENT_ID,
code_verifier: codeVerifier, // Enviar verifier en lugar de secret
}),
});
3. Client Credentials Grant
Se usa para comunicacion servidor a servidor (M2M: Machine to Machine). No hay usuario involucrado.
// Autenticacion entre servicios backend
const tokenResponse = await fetch('https://auth.example.com/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': `Basic ${Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString('base64')}`,
},
body: new URLSearchParams({
grant_type: 'client_credentials',
scope: 'api:read api:write',
}),
});
const { access_token } = await tokenResponse.json();
4. Refresh Token Grant
Flujo para renovar el access token.
// Renovacion de access token mediante refresh token
const refreshResponse = await fetch('https://auth.example.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: storedRefreshToken,
client_id: CLIENT_ID,
}),
});
const { access_token, refresh_token } = await refreshResponse.json();
// Si se devuelve un nuevo refresh token, actualizar el almacenamiento
OpenID Connect (OIDC)
OIDC es una capa de autenticacion construida sobre OAuth 2.0.
Estructura del ID Token
En OIDC, ademas del access token, se emite un ID Token (formato JWT).
flowchart TB
subgraph JWT["Estructura del ID Token (JWT)"]
subgraph Header["Header"]
H1["alg: RS256, typ: JWT, kid: key-id-123"]
end
subgraph Payload["Payload"]
P1["iss: Emisor"]
P2["sub: Sujeto"]
P3["aud: Audiencia"]
P4["exp: Fecha de expiracion"]
P5["iat: Fecha de emision"]
P6["nonce: Contramedida contra ataques de replay"]
P7["email, name: Informacion del usuario"]
end
subgraph Signature["Signature (Firma)"]
S1["Firmado con la clave privada del servidor de autorizacion"]
end
Header --> Payload --> Signature
end
Verificacion del ID Token
import jwt from 'jsonwebtoken';
import jwksClient from 'jwks-rsa';
// Configuracion del cliente JWKS (JSON Web Key Set)
const client = jwksClient({
jwksUri: 'https://auth.example.com/.well-known/jwks.json',
cache: true,
rateLimit: true,
});
// Funcion para obtener la clave publica
const getKey = (header: jwt.JwtHeader, callback: jwt.SigningKeyCallback) => {
client.getSigningKey(header.kid, (err, key) => {
callback(err, key?.getPublicKey());
});
};
// Verificacion del ID Token
const verifyIdToken = (idToken: string): Promise<jwt.JwtPayload> => {
return new Promise((resolve, reject) => {
jwt.verify(
idToken,
getKey,
{
algorithms: ['RS256'],
issuer: 'https://auth.example.com',
audience: CLIENT_ID,
},
(err, decoded) => {
if (err) reject(err);
else resolve(decoded as jwt.JwtPayload);
}
);
});
};
Endpoint UserInfo
Se puede obtener informacion adicional del usuario usando el access token.
const userInfoResponse = await fetch('https://auth.example.com/userinfo', {
headers: {
'Authorization': `Bearer ${accessToken}`,
},
});
const userInfo = await userInfoResponse.json();
// {
// "sub": "user-123",
// "name": "Juan Garcia",
// "email": "user@example.com",
// "email_verified": true,
// "picture": "https://example.com/avatar.jpg"
// }
Diseno de Scopes
Scopes Estandar de OIDC
| Scope | Claims devueltos |
|---|---|
openid | sub (obligatorio) |
profile | name, family_name, given_name, picture, etc. |
email | email, email_verified |
address | address |
phone | phone_number, phone_number_verified |
Ejemplo de Scopes Personalizados
// Scopes personalizados para acceso a API
const scopes = [
'openid', // Obligatorio para OIDC
'profile', // Perfil basico
'email', // Direccion de correo
'api:read', // Permiso de lectura API
'api:write', // Permiso de escritura API
'admin:users', // Permiso de administracion de usuarios
];
Mejores Practicas de Seguridad
1. Proteccion CSRF con Parametro State
// Generar state unico por sesion
const generateState = (): string => {
const state = crypto.randomBytes(32).toString('hex');
// Guardar en sesion
session.oauthState = state;
return state;
};
// Validar state en callback
const validateState = (receivedState: string): boolean => {
const isValid = receivedState === session.oauthState;
delete session.oauthState; // Eliminar despues de usar
return isValid;
};
2. Proteccion contra Ataques de Replay con Nonce
// Incluir nonce en la solicitud de ID Token
const nonce = crypto.randomBytes(16).toString('hex');
session.oidcNonce = nonce;
authUrl.searchParams.set('nonce', nonce);
// Verificar nonce al validar el ID Token
const decoded = await verifyIdToken(idToken);
if (decoded.nonce !== session.oidcNonce) {
throw new Error('Invalid nonce');
}
3. Almacenamiento Seguro de Tokens
// Backend: Gestion de sesion con HTTPOnly Cookie
res.cookie('session_id', sessionId, {
httpOnly: true, // No accesible desde JavaScript
secure: true, // Requiere HTTPS
sameSite: 'lax', // Proteccion CSRF
maxAge: 3600000, // 1 hora
});
// Frontend (SPA): Mantener en memoria
// Evitar LocalStorage ya que es vulnerable a XSS
class TokenStore {
private accessToken: string | null = null;
setToken(token: string) {
this.accessToken = token;
}
getToken() {
return this.accessToken;
}
}
4. Diseno de Tiempos de Expiracion de Tokens
Tiempos de expiracion recomendados:
| Token | Tiempo de expiracion |
|---|---|
| Access Token | 15 minutos - 1 hora |
| Refresh Token | 7 - 30 dias (expiracion absoluta) |
| ID Token | 5 minutos - 1 hora |
| Codigo de autorizacion | Menos de 10 minutos |
Discovery Document
Los proveedores OIDC proporcionan un endpoint Discovery para obtener automaticamente la informacion de configuracion.
// Obtener configuracion del endpoint Well-known
const discoveryUrl = 'https://auth.example.com/.well-known/openid-configuration';
const config = await fetch(discoveryUrl).then(r => r.json());
// {
// "issuer": "https://auth.example.com",
// "authorization_endpoint": "https://auth.example.com/authorize",
// "token_endpoint": "https://auth.example.com/token",
// "userinfo_endpoint": "https://auth.example.com/userinfo",
// "jwks_uri": "https://auth.example.com/.well-known/jwks.json",
// "scopes_supported": ["openid", "profile", "email"],
// "response_types_supported": ["code", "token", "id_token"],
// ...
// }
Resumen
OAuth 2.0 y OIDC son las tecnologias fundamentales de autenticacion y autorizacion web moderna.
Puntos Clave de OAuth 2.0
- Proposito: Autorizacion (delegacion de permisos de acceso a recursos)
- Flujo recomendado: Authorization Code Grant + PKCE
- Tokens: Access Token, Refresh Token
Puntos Clave de OIDC
- Proposito: Autenticacion (verificacion de identidad del usuario)
- Elemento adicional: ID Token (formato JWT)
- Scopes estandar: openid, profile, email, etc.
Requisitos de Seguridad Esenciales
- Uso de PKCE (especialmente en SPA/movil)
- Proteccion CSRF con parametro State
- Proteccion contra ataques de replay con Nonce
- Almacenamiento de tokens con HTTPOnly Cookie
- Tiempos de expiracion cortos para Access Tokens
Comprendiendo e implementando correctamente estos conceptos, se puede construir un sistema de autenticacion seguro y facil de usar.
Enlaces de Referencia
- RFC 6749 - OAuth 2.0
- RFC 7636 - PKCE
- OpenID Connect Core 1.0
- OAuth 2.0 Security Best Current Practice