Funcionamiento de OAuth 2.0 y OpenID Connect - Principios de Diseno de Autenticacion y Autorizacion

18 min de lectura | 2025.12.02

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.

ElementoOAuth 2.0OpenID Connect
PropositoAutorizacion (Authorization)Autenticacion (Authentication)
Pregunta que responde”Permite a esta app acceder a los recursos?""Quien eres tu?”
Que se obtieneAccess TokenID Token + Access Token
Ano de especificacion2012 (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

ScopeClaims devueltos
openidsub (obligatorio)
profilename, family_name, given_name, picture, etc.
emailemail, email_verified
addressaddress
phonephone_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:

TokenTiempo de expiracion
Access Token15 minutos - 1 hora
Refresh Token7 - 30 dias (expiracion absoluta)
ID Token5 minutos - 1 hora
Codigo de autorizacionMenos 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

  1. Uso de PKCE (especialmente en SPA/movil)
  2. Proteccion CSRF con parametro State
  3. Proteccion contra ataques de replay con Nonce
  4. Almacenamiento de tokens con HTTPOnly Cookie
  5. 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

← Volver a la lista