Patrón Voucher
Valida claims y delega acceso usando vouchers firmados sin exponer datos sensibles. Un patrón de seguridad para autorización basada en tokens entre servicios.
Visión General
El patrón Voucher emite tokens firmados y de corta duración (vouchers) que prueban un claim sin revelar los datos sensibles subyacentes. Un servicio puede emitir un voucher afirmando “este usuario está autenticado” o “esta petición está autorizada” y pasarlo a servicios downstream. Los servicios downstream verifican la firma del voucher y confían en el claim — nunca necesitan acceso a las credenciales originales o al data store.
Cuándo Usar
Usar el patrón Voucher cuando:
- Un servicio necesita probar un claim a otro servicio sin compartir datos sensibles
- Quieres evitar pasar credenciales a través de múltiples hops de servicio
- Los servicios downstream necesitan verificar autorización sin consultar una base de datos central
- Necesitas tokens de delegación de corta duración y scoped entre microservicios
- Quieres reducir el acoplamiento entre servicios y el identity store
Solución
Python
import hmac
import hashlib
import json
import time
import base64
from dataclasses import dataclass, asdict
SECRET_KEY = b"shared-secret-key-between-services"
@dataclass
class Voucher:
issuer: str
subject: str
claims: dict
issued_at: float
expires_at: float
def to_dict(self) -> dict:
return asdict(self)
def is_expired(self) -> bool:
return time.time() > self.expires_at
def sign_voucher(voucher: Voucher) -> str:
payload = json.dumps(voucher.to_dict(), sort_keys=True).encode()
signature = hmac.new(SECRET_KEY, payload, hashlib.sha256).digest()
token = base64.urlsafe_b64encode(payload) + b"." + base64.urlsafe_b64encode(signature)
return token.decode()
def verify_voucher(token: str) -> Voucher | None:
try:
payload_b64, sig_b64 = token.rsplit(".", 1)
payload = base64.urlsafe_b64decode(payload_b64)
expected_sig = hmac.new(SECRET_KEY, payload, hashlib.sha256).digest()
actual_sig = base64.urlsafe_b64decode(sig_b64)
if not hmac.compare_digest(expected_sig, actual_sig):
return None
data = json.loads(payload)
voucher = Voucher(**data)
if voucher.is_expired():
return None
return voucher
except (ValueError, json.JSONDecodeError, KeyError):
return None
# Servicio A: Emitir un voucher después de autenticar un usuario
def issue_voucher(user_id: str, permissions: list[str]) -> str:
voucher = Voucher(
issuer="auth-service",
subject=user_id,
claims={"permissions": permissions, "tenant": "acme"},
issued_at=time.time(),
expires_at=time.time() + 300, # 5 minutos
)
return sign_voucher(voucher)
# Servicio B: Verificar el voucher sin acceder a la base de datos de auth
def handle_request(auth_header: str):
token = auth_header.replace("Bearer ", "")
voucher = verify_voucher(token)
if not voucher:
raise PermissionError("Invalid or expired voucher")
if "read:reports" not in voucher.claims.get("permissions", []):
raise PermissionError("Insufficient permissions")
return f"Report delivered to {voucher.subject}"
# Uso
token = issue_voucher("user-123", ["read:reports", "write:reports"])
result = handle_request(f"Bearer {token}")
print(result)
JavaScript
const crypto = require("crypto");
const SECRET_KEY = "shared-secret-key-between-services";
function signVoucher(voucher) {
const payload = JSON.stringify(voucher);
const signature = crypto
.createHmac("sha256", SECRET_KEY)
.update(payload)
.digest("base64url");
return Buffer.from(payload).toString("base64url") + "." + signature;
}
function verifyVoucher(token) {
try {
const [payloadB64, sig] = token.split(".");
const payload = JSON.parse(Buffer.from(payloadB64, "base64url").toString());
const expectedSig = crypto
.createHmac("sha256", SECRET_KEY)
.update(Buffer.from(payloadB64, "base64url"))
.digest("base64url");
if (!crypto.timingSafeEqual(
Buffer.from(sig),
Buffer.from(expectedSig)
)) {
return null;
}
if (Date.now() / 1000 > payload.expires_at) {
return null;
}
return payload;
} catch {
return null;
}
}
// Servicio A: Emitir voucher
function issueVoucher(userId, permissions) {
const voucher = {
issuer: "auth-service",
subject: userId,
claims: { permissions, tenant: "acme" },
issued_at: Date.now() / 1000,
expires_at: Date.now() / 1000 + 300,
};
return signVoucher(voucher);
}
// Servicio B: Verificar voucher
function handleRequest(authHeader) {
const token = authHeader.replace("Bearer ", "");
const voucher = verifyVoucher(token);
if (!voucher) {
throw new Error("Invalid or expired voucher");
}
if (!voucher.claims.permissions.includes("read:reports")) {
throw new Error("Insufficient permissions");
}
return `Report delivered to ${voucher.subject}`;
}
// Uso
const token = issueVoucher("user-123", ["read:reports", "write:reports"]);
console.log(handleRequest(`Bearer ${token}`));
Java
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.List;
import java.util.Map;
public class VoucherPattern {
static final String SECRET = "shared-secret-key-between-services";
record Voucher(String issuer, String subject, Map<String, Object> claims,
long issuedAt, long expiresAt) {}
static String signVoucher(Voucher v) throws Exception {
String payload = String.format(
"{\"issuer\":\"%s\",\"subject\":\"%s\",\"claims\":%s,\"issuedAt\":%d,\"expiresAt\":%d}",
v.issuer(), v.subject(), v.claims().toString(), v.issuedAt(), v.expiresAt()
);
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(SECRET.getBytes(), "HmacSHA256"));
byte[] sig = mac.doFinal(payload.getBytes(StandardCharsets.UTF_8));
String payloadB64 = Base64.getEncoder().encodeToString(payload.getBytes());
String sigB64 = Base64.getEncoder().encodeToString(sig);
return payloadB64 + "." + sigB64;
}
static boolean verifyVoucher(String token) throws Exception {
String[] parts = token.split("\\.");
if (parts.length != 2) return false;
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(SECRET.getBytes(), "HmacSHA256"));
byte[] expectedSig = mac.doFinal(Base64.getDecoder().decode(parts[0]));
byte[] actualSig = Base64.getDecoder().decode(parts[1]);
return java.util.Arrays.equals(expectedSig, actualSig);
}
public static void main(String[] args) throws Exception {
Voucher v = new Voucher(
"auth-service", "user-123",
Map.of("permissions", List.of("read:reports")),
System.currentTimeMillis() / 1000,
System.currentTimeMillis() / 1000 + 300
);
String token = signVoucher(v);
System.out.println("Valid: " + verifyVoucher(token));
}
}
Explicación
El patrón Voucher crea una cadena de confianza entre servicios sin compartir datos sensibles:
- Issuer: El servicio que autentica al usuario o verifica el claim. Crea y firma el voucher.
- Voucher: Un token firmado que contiene claims (user ID, permisos, tenant), issuer y expiración. Firmado con HMAC o clave asimétrica.
- Verifier: El servicio downstream que recibe el voucher. Verifica la firma y chequea expiración. Confía en el claim sin consultar el identity store.
- Short-lived: Los vouchers expiran rápido (minutos, no horas). Esto limita la ventana de misuse si un voucher es interceptado.
- Scoped: Los vouchers contienen solo los claims necesarios para la operación downstream. Sin contraseñas, sin session tokens, sin datos innecesarios.
Variantes
| Variante | Método de Firma | Caso de Uso |
|---|---|---|
| HMAC Voucher | Shared secret (HMAC-SHA256) | Servicios que comparten una secret key |
| JWT Voucher | Asimétrico (RS256, ES256) | Verificación pública con firma privada |
| Capability Token | Token opaco + lookup | Cuando los claims son demasiado grandes para un token |
| Delegation Token | Firmado por servicio intermedio | Cadena de confianza across múltiples hops |
Pautas
- Mantener vouchers de corta duración — 5 minutos o menos. Limitar el daño de tokens interceptados.
- Incluir solo claims necesarios — no embeber perfiles de usuario o datos sensibles en el voucher
- Usar firma asimétrica para cross-org — RS256/ES256 permite a verificadores chequear solo con public key
- Usar HMAC para servicios internos — más simple, más rápido, shared secret vía secret manager
- Verificar firma y expiración en cada petición — nunca skip verification para callers “trusted”
- Rotar signing keys regularmente — el compromiso de la key compromete todos los vouchers
- Incluir audience claim — un voucher para el Servicio B no debería ser aceptado por el Servicio C
- Loguear emisión de vouchers — trackear quién emitió qué voucher para audit trails
Errores Comunes
- Hacer vouchers de larga duración — se convierten en session tokens reutilizables, derrotando el propósito
- Embeber datos sensibles (contraseñas, PII) en claims del voucher — los vouchers pueden ser logueados
- No verificar la firma — aceptar vouchers sin firmar o tampered
- No chequear expiración — vouchers stale otorgan acceso después de deberían haber expirado
- Usar la misma key para todos los servicios — un compromiso rompe toda la cadena
- No incluir audience claim — vouchers destinados a un servicio son aceptados por otro
- Pasar vouchers en URL query strings — las URLs son logueadas por proxies y load balancers
- No rotar keys — keys de larga duración aumentan el blast radius de un compromiso
Troubleshooting
- Login works for some users but not others: check identity provider configuration, user claims, and role mappings. Look for case sensitivity in identifiers.
- Token expires too quickly: verify token lifetime, refresh logic, and clock skew. Short tokens with secure refresh are preferred.
- Session is not shared across subdomains: set the cookie domain and SameSite policy correctly. Test in the target browser.
- Brute force attempts increase: implement rate limiting, account lockout, and CAPTCHA. Monitor failed authentication patterns.
- OIDC flow fails with invalid_state: ensure the state parameter is stored, transmitted, and validated in the same user session.
Errores Comunes en Producción
- Aplicar el patrón donde no se necesita abstracción, agregando complejidad accidental.
- Dejar que el patrón se filtre en módulos no relacionados y confundir los límites de responsabilidad.
- Sobre-ingeniería en la primera implementación en lugar de comenzar simple y medir el dolor.
- Saltar los tests de contrato, de modo que las refactorizaciones rompan consumidores en silencio.
- Ignorar modos de fallo que el patrón no cubre.
- Usar el patrón como opción por defecto en lugar de elegir la herramienta adecuada para la escala actual.
- Olvidar documentar cuándo dejar de usar el patrón y qué lo reemplaza.
- Carecer de observabilidad sobre rendimiento y propagación de errores del patrón.
Preguntas frecuentes
¿Es este patrón adecuado para proyectos pequeños?
Para proyectos pequeños con pocos componentes, este patrón puede añadir complejidad innecesaria. Empieza simple e introduce el patrón cuando sientas el problema que resuelve.
¿Cómo se compara este patrón con alternativas?
Cada patrón hace diferentes trade-offs. Revisa la tabla de variantes arriba y considera tus restricciones específicas: tamaño del equipo, requisitos de rendimiento y planes de escalado.
¿Puedo aplicar este patrón parcialmente?
Sí. Muchos equipos adoptan patrones incrementalmente. Empieza con la idea central y añade sofisticación según sea necesario. El patrón es una guía, no un blueprint estricto.
Recursos Relacionados
Patrón Federated Identity
Delega la autenticación a proveedores de identidad externos. Un patrón para integrar OAuth2, OIDC, SAML y SSO entre múltiples servicios y organizaciones.
PatternPatrón Circuit Breaker
Previene fallos en cascada deteniendo solicitudes a servicios que están fallando. Un patrón arquitectural para sistemas distribuidos resilientes.
PatternPatrón Ambassador: Offloadeá Cross-Cutting Concerns a un
Cómo offloadar cross-cutting concerns a un proxy ambassador. Cubre connection pooling, retry logic, circuit breaking, monitoring, y TLS termination para client services.
PatternPatrón Multi-Tenant Data Isolation
Aisla datos de tenants en infraestructura compartida usando row-level security, schema-per-tenant o database-per-tenant. Un patrón para aplicaciones SaaS.
RecipeEscapar Entidades HTML
Cómo escapar entidades HTML para prevenir ataques XSS en Python, Java y JavaScript.
RecipeSanitizar Input de Usuario
Cómo sanitizar y validar input de usuario en Python, Java y JavaScript para prevenir ataques de inyección.