OAuth 2.0 y JWT: La guía completa para la seguridad de las APIs
En cualquier sistema distribuido, el contrato definido por una API solo es tan sólido como su modelo de seguridad. Los puntos finales sin protección o con medidas de seguridad inadecuadas representan una vulnerabilidad crítica, exponiendo datos y lógica empresarial a accesos no autorizados. Para los directores técnicos y líderes de ingeniería, establecer una arquitectura de seguridad robusta, escalable y conforme a las normas de la industria no es una opción; es un requisito fundamental.OAuth 2.0 para la autorización y JSON Web Tokens (JWT) para los credenciales de acceso han surgido como la solución definitiva para este desafío.
Este artículo proporciona un esquema técnico detallado para implementar la seguridad de API utilizando OAuth 2.0 y JWT. Analizaremos los aspectos técnicos en profundidad, explorando el flujo arquitectónico, los pasos de validación críticos y proporcionando ejemplos de código prácticos para construir un servidor de recursos seguro.
Los Componentes Clave: Marco y Token
Es fundamental comprender que OAuth 2.0 y JWT tienen propósitos distintos pero complementarios. Confundir uno con el otro es un error arquitectónico común.
OAuth 2.0: El marco de autorización
OAuth 2.0 es unmarco de autorización, no un protocolo de autenticación. Su propósito principal es permitir que una aplicación cliente acceda a los recursos en nombre de un usuario (el propietario de los recursos) sin exponer las credenciales del usuario al cliente.
El marco define cuatro roles clave:
- Propietario de los recursos: El usuario que posee los datos y otorga permisos.
- Cliente: La aplicación (por ejemplo, una interfaz web o una aplicación móvil) que solicita el acceso a los datos del propietario de los recursos.
- Servidor de autorización (AS): El servidor que autentica al propietario de los recursos y emite tokens de acceso después de que se haya otorgado la aprobación. Este es el principal centro de seguridad.
- Servidor de recursos (RS): El servidor API que aloja los recursos protegidos y acepta/valida los tokens de acceso. Este es el servicio backend que estás desarrollando.
Servicios de Ingeniería de Productos
Trabaje con nuestros gestores de proyectos, ingenieros de software y probadores de calidad internos para desarrollar su nuevo producto de software personalizado o para apoyar su flujo de trabajo actual, siguiendo metodologías Agile, DevOps y Lean.
Para clientes públicos modernos (como SPAs o aplicaciones móviles), el flujo recomendado es "Authorization Code Grant con Proof Key for Code Exchange (PKCE)Entrega de código de autorización con clave de prueba para el intercambio de códigos (PKCE)". Proporciona una forma segura de obtener tokens sin exponer ninguna clave del cliente en el agente, lo que ayuda a mitigar los ataques de interceptación de códigos de autorización.
Tokens de Web JSON (JWT): El Credencial de Acceso
Un JWT es un medio compacto y seguro para URL, que permite representar las reclamaciones para ser transferidas entre dos partes. En nuestro contexto, es el formato del token de acceso emitido por el Servidor de Autorización. Su naturaleza sin estado es su característica más poderosa: el Servidor de Recursos puede validar un JWT y determinar la identidad y los permisos del usuario sin necesidad de realizar una llamada a la base de datos o contactar al Servidor de Autorización.
Un JWT consta de tres partes, separadas por puntos:.
- Encabezado: Contiene metadatos sobre el token, incluyendo el algoritmo de firma (alg, p.ej., RS256) y el tipo de token (typ, p.ej., JWT).{"alg": "RS256", "typ": "JWT"}
- Carga útil: Contiene las reclamaciones, que son declaraciones sobre la entidad (típicamente el usuario) y metadatos adicionales. Las reclamaciones estándar incluyen:
iss(Emisor): El Servidor de Autorización que emitió el token.sub(Sujeto): El identificador único del propietario del recurso.aud(Audiencia): El destinatario previsto del token (su Servidor de Recursos/API).exp(Fecha de expiración): La marca de tiempo después de la cual el token deja de ser válido.iat(Emitido en): La marca de tiempo cuando se emitió el token.scp(Alcance): Una lista separada por espacios de permisos otorgados por el usuario.
- Firma: Una firma criptográfica utilizada para verificar la integridad del token. Se crea firmando el encabezado y la carga útil codificados con una clave privada que posee el Servidor de Autorización. El Servidor de Recursos utiliza la clave pública correspondiente para validarla.HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
Planos arquitectónicos: El flujo de autorización + PKCE
Analicemos el flujo completo, centrándonos en las interacciones entre el cliente, el Servidor de Autorización y tu Servidor de Recursos.
- El cliente inicia el flujo: La aplicación del cliente genera primero una cadena aleatoria de forma criptográfica llamada
code_verifier. A continuación, crea uncode_challengemediante el hash SHA256 del verificador y la codificación Base64Url del resultado. - Redireccionar al servidor de autorización: El cliente redirige el navegador del usuario al punto final
/authorizedel Servidor de Autorización, incluyendo parámetros comoclient_id,redirect_uri,scope,response_type=code, y el generadocode_challengeycode_challenge_method=S256. - Autenticación del usuario y consentimiento: El usuario interactúa con el Servidor de Autorización, introduciendo sus credenciales (autenticación) y aprobando los permisos (scopes) solicitados por el cliente (consentimiento).
- Concesión de código de autorización: Una vez que se ha autenticado correctamente y se ha dado el consentimiento, el Servidor de Autorización redirige al usuario de vuelta al
redirect_uridel cliente con un código de autorización temporal y de uso único.Intercambio de tokens: El backend del cliente envía una solicitud POST al punto final /token del Servidor de Autorización. Esta solicitud se realiza de servidor a servidor (no a través del navegador) y incluye el código de autorización recibido junto con el original code_verifier.El Servidor de Autorización valida el code_verifier contra el code_challenge de la solicitud inicial. Si coinciden, esto demuestra que la solicitud del token proviene del mismo cliente que inició el flujo. A continuación, devuelve un token de acceso (JWT) y un token de actualización.API con JWT: El cliente almacena el token de acceso e incluye este en el encabezado Authorization de todas las solicitudes a tu API protegida (el Servidor de Recursos).Autorización: Bearer <your_jwt_here> - Validación del JWT por parte del Servidor de Recursos:
- Este es el paso más crítico para tu API. Al recibir una solicitud, tu Servidor de Recursos debe realizar una serie de validaciones sin estado
- antes de ejecutar cualquier lógica empresarial:Verificar la firma: Obtén la clave pública del Servidor de Autorización (normalmente desde el punto final .well-known/jwks.json) y utilízala para verificar la firma del JWT. Esto demuestra que el token ha sido emitido por el AS de confianza y no ha sido alterado.
- Validar las reclamaciones estándar: Comprueba si la reclamación
iss(emisor) coincide con el Servidor de Autorización esperado.Comprueba si la reclamación - aud
- (audiencia) coincide con el identificador único de tu API. Comprueba si la hora actual es anterior al
exp(fecha de caducidad).Comprueba los permisos para la autorización: - Después de validar, inspecciona la reclamación
scppara asegurarte de que el token otorga los permisos necesarios para la operación solicitada. Por ejemplo, una solicitud para - DELETE /api/users/123
podría requerir el permisoscope
- (audiencia) coincide con el identificador único de tu API. Comprueba si la hora actual es anterior al
- scpDespués de la validación, inspeccione el
SCPsolicitar para asegurar que el token otorga los permisos necesarios para la operación solicitada. Por ejemplo, una solicitud deELIMINAR /api/users/123podría requerirusuarios: eliminaralcance.
- Validar las reclamaciones estándar: Comprueba si la reclamación
Solo si todas estas comprobaciones sean exitosas, se procesará la solicitud.
Implementación práctica: Asegurar una API de Python
Implementemos la lógica de validación JWT (Paso 7) en una API de Python utilizando FastAPI. Este ejemplo asume que tiene un Servidor de Autorización (como Auth0, Okta o Keycloak) que proporciona un punto final JWKS.
Dependencias
pip install "fastapi[all]" "python-jose[cryptography]" "requests"
Lógica de validación JWT
Crearemos una dependencia reutilizable que gestione la extracción y validación de tokens. Este código obtiene las claves públicas del URI JWKS, las almacena en caché para evitar la latencia de red en cada solicitud, y realiza los pasos de validación.
# src/security.py
import os
import requests
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from jose import jwt, JWTError
from functools import lru_cache
# --- Configuration ---
# In a real app, load these from environment variables or a config file.
AUTH_SERVER_URL = "https://your-auth-server.com/"
API_AUDIENCE = "https://api.yourapp.com"
ALGORITHMS = ["RS256"]
oauth2_scheme = OAuth2PasswordBearer(tokenUrl=f"{AUTH_SERVER_URL}oauth/token")
# --- JWKS Caching and Public Key Retrieval ---
@lru_cache()
def get_jwks():
"""
Fetches the JSON Web Key Set (JWKS) from the authorization server.
Uses lru_cache for in-memory caching to improve performance.
"""
try:
response = requests.get(f"{AUTH_SERVER_URL}.well-known/jwks.json")
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail=f"Could not connect to Authorization Server: {e}"
)
def get_signing_key(token: str):
"""
Finds the appropriate public key from the JWKS to verify the token's signature.
"""
try:
unverified_header = jwt.get_unverified_header(token)
except JWTError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token header"
)
rsa_key = {}
jwks = get_jwks()
for key in jwks["keys"]:
if key["kid"] == unverified_header["kid"]:
rsa_key = {
"kty": key["kty"],
"kid": key["kid"],
"use": key["use"],
"n": key["n"],
"e": key["e"],
}
if not rsa_key:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Signing key not found in JWKS"
)
return rsa_key
# --- Main Token Validation Dependency ---
async def validate_token(token: str = Depends(oauth2_scheme)) -> dict:
"""
Validates a JWT token and returns its payload if valid.
This function acts as a FastAPI dependency.
"""
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
signing_key = get_signing_key(token)
try:
payload = jwt.decode(
token,
signing_key,
algorithms=ALGORITHMS,
audience=API_AUDIENCE,
issuer=AUTH_SERVER_URL
)
return payload
except jwt.ExpiredSignatureError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Token has expired"
)
except jwt.JWTClaimsError as e:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=f"Invalid claims: {e}"
)
except JWTError:
raise credentials_exception
Servicios de Ingeniería de Productos
Colabore con nuestros gestores de proyectos, ingenieros de software y probadores de calidad para desarrollar su nuevo producto de software personalizado o para apoyar su flujo de trabajo actual, siguiendo metodologías Agile, DevOps y Lean.
Proteger un punto final
Con la dependencia de validación creada, asegurar un punto final es limpio y declarativo. También podemos añadir otra dependencia para verificar los permisos necesarios.
# src/main.py
from fastapi import FastAPI, Depends, HTTPException, status
from pydantic import BaseModel
from typing import List
from .security import validate_token
app = FastAPI()
class Item(BaseModel):
id: int
name: str
# --- Scope Checking Dependency ---
def require_scope(required_scopes: List[str]):
"""
A dependency factory to check if the token has the required scopes.
"""
async def scope_checker(payload: dict = Depends(validate_token)):
token_scopes = payload.get("scope", "").split()
for scope in required_scopes:
if scope not in token_scopes:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=f"Missing required scope: {scope}"
)
return payload
return scope_checker
# --- Protected Endpoints ---
@app.get("/api/items", response_model=List[Item])
async def read_items(
# This endpoint requires a valid token with the "items:read" scope
payload: dict = Depends(require_scope(["items:read"]))
):
# The `sub` claim from the token payload can be used to identify the user
user_id = payload.get("sub")
print(f"Fetching items for user: {user_id}")
return [{"id": 1, "name": "Widget"}, {"id": 2, "name": "Gadget"}]
@app.post("/api/items", response_model=Item)
async def create_item(
item: Item,
# This endpoint requires a valid token with the "items:write" scope
payload: dict = Depends(require_scope(["items:write"]))
):
print(f"User {payload.get('sub')} is creating an item.")
# Business logic to create the item...
return item
Esta implementación demuestra un patrón robusto y reutilizable. La lógica de seguridad está separada de la lógica empresarial, y los puntos finales especifican de forma declarativa sus requisitos de seguridad.
Consideraciones finales
- Duración del token: Mantenga las duraciones de los tokens de acceso cortas (por ejemplo, entre 5 y 15 minutos) para limitar el impacto de un token comprometido. Utilice tokens de refresco con larga duración, almacenados de forma segura en el cliente, para obtener nuevos tokens de acceso sin necesidad de que el usuario se autentique nuevamente.
- Revocación del token: Si bien los JWT son sin estado, puede que necesite un mecanismo para revocar el acceso antes de su expiración (por ejemplo, cuando un usuario cierra sesión o cambia su contraseña). Esto normalmente requiere introducir una capa con estado, como una lista de revocación verificada por la API, lo que intercambia algunas de las ventajas del funcionamiento sin estado por un mayor control de seguridad.
- No almacene datos confidenciales en los JWT: El payload del JWT está codificado en Base64Url, no está cifrado. Es de lectura pública. Nunca almacene información sensible como contraseñas o datos personales directamente en el payload.
Al implementar el marco de trabajo OAuth 2.0 con JWTs, se crea un modelo de seguridad para la API que no solo es robusto y cumple con los estándares modernos, sino también altamente escalable y desacoplado, lo que permite que sus servicios crezcan de forma segura.
Preguntas frecuentes
¿Cuál es la diferencia entre OAuth 2.0 y JWT?
OAuth 2.0 es un marco de autorización, no un protocolo de autenticación. Su propósito principal es permitir que una aplicación cliente acceda a los recursos en nombre de un usuario sin exponer las credenciales del mismo. Define los roles y los flujos para delegar el acceso. Un JSON Web Token (JWT), por otro lado, es un formato de credenciales. Es un token compacto y seguro que puede utilizarse en URL y representa reclamaciones entre dos partes. En este contexto, el JWT es el token de acceso emitido por un servidor de autorización OAuth 2.0, que la API (Servidor de Recursos) puede validar para confirmar la identidad y los permisos del usuario.
¿Cómo valida una API un Token Web JSON (JWT)?
Una API (o Servidor de Recursos) debe realizar varios pasos clave de validación antes de confiar en un JWT y procesar una solicitud.
- Verificar la Firma: El servidor obtiene la clave pública del servidor de autorización (a menudo desde el
.well-known/jwks.json endpoint) y la utiliza para verificar criptográficamente que la firma del token es válida y fue emitida por la autoridad de confianza.Validar las Afirmaciones Estándar: El servidor verifica las afirmaciones críticas en el payload del token, como la iss (emisor) para asegurarse de que proviene de la fuente correcta, la aud (audiencia) para garantizar que el token está destinado a esta API específica, y la exp (fecha de expiración) para asegurarse de que el token no ha caducado.Comprobar los Alcances: Después de la validación, el servidor inspecciona la afirmación scp (alcance) para asegurarse de que el token otorga los permisos suficientes para la operación específica solicitada (por ejemplo, users:delete).¿Qué es el flujo de Autorización con Código y PKCE?
El flujo de Autorización con Código y Prueba Clave para Intercambio (PKCE) es el flujo OAuth 2.0 recomendado y más seguro para aplicaciones modernas como las aplicaciones de una sola página (SPAs) y aplicaciones móviles.
- La aplicación cliente genera un secreto
code_verifiery una versión transformada llamada uncode_challenge. - Redirige al usuario al servidor de autorización con el
code_challenge. - El usuario inicia sesión y otorga el consentimiento.
- El servidor envía un código de autorización temporal al cliente.
- El cliente envía este
authorization_codey el originalcode_verifieral punto final del token. - El servidor de autorización valida el
code_verifiercontra elcode_challengeque almacenó. Si coinciden, emite el token de acceso (JWT). Este proceso garantiza que incluso si elauthorization_codees interceptado, no sirve sin elcode_verifier, lo que evita los ataques de interceptación.