Quantum-Safe Cloud -- Parte 4
QuantumID: Autenticación Preparada para lo que Viene
El Problema
JWT está en todas partes. Si has construido una API en los últimos diez años, casi seguro que lo has usado. Es simple, stateless y tiene buen soporte.
Pero JWT tiene problemas en los que la mayoría de desarrolladores no piensan hasta que algo se rompe.
Problema 1: No hay revocación. Un JWT es válido hasta que expira. Si el token de un usuario es robado — de un navegador, de un fichero de log, de un dispositivo comprometido — el atacante puede usarlo hasta que caduque. No hay una forma estándar de invalidar un token específico sin una blocklist de tokens (lo cual anula la ventaja de ser stateless que hacía JWT atractivo en primer lugar).
Problema 2: Confusión de algoritmo. La cabecera del JWT incluye el campo alg, y algunas librerías JWT confían en él. Si tu librería acepta alg: "none", un atacante puede eliminar la firma. Si acepta tanto RS256 como HS256, un atacante puede firmar con la clave pública como secreto HMAC. Estas vulnerabilidades han causado incidentes reales en sistemas en producción.
Problema 3: El problema cuántico. RS256 (el algoritmo de firma JWT más común) usa RSA. Los tokens firmados con RS256 hoy serán falsificables por un ordenador cuántico en el futuro. Un atacante que recopile tokens firmados ahora puede verificarlos contra tokens falsificados pero válidos después, o falsificar nuevos una vez que RSA se rompa.
La solución no es parchear JWT. Es usar un sistema de identidad adecuado donde estas decisiones se toman correctamente por diseño, y donde la criptografía esté preparada para el futuro.
QuantumID es un proveedor de identidad OIDC/OAuth2 construido sobre esas bases. Los tokens se firman con ML-DSA (quantum-safe). Las sesiones se gestionan en el servidor (la revocación funciona). El algoritmo no es configurable por el cliente (sin confusión de algoritmo). Y como es OIDC, integrarlo en una aplicación .NET es estándar — AddOpenIdConnect, unas pocas opciones, listo.
La Solución
OIDC (OpenID Connect) es una capa de identidad sobre OAuth2. Tu aplicación delega la autenticación a un proveedor de identidad (QuantumID) y recibe un JWT ID token. La diferencia con hacerlo tú mismo:
- El token lo emite una autoridad de confianza (QuantumID), no tu aplicación
- La firma usa ML-DSA — quantum-safe y no configurable por quien llama
- Las sesiones existen en el servidor — la revocación funciona
- Los scopes controlan a qué da acceso el token
- El flujo PKCE previene la interceptación del authorization code
En la práctica, para una API .NET, esto significa:
- Eliminar tu código personalizado de emisión JWT
- Añadir
AddOpenIdConnectapuntando a QuantumID - Tu API valida tokens usando las claves públicas de QuantumID (vía el endpoint JWKS)
- Login, logout y renovación de tokens los gestiona QuantumID
Tu aplicación deja de ser un proveedor de identidad. Se convierte en una relying party. Esta es la división correcta de responsabilidades.
Execute
Vamos a reemplazar la implementación JWT personalizada en la API de usuarios con QuantumID.
ATLAS para esta tarea
[A] ARCHITECT
Replace DIY JWT with QuantumID as the OIDC provider.
The API becomes a resource server (validates tokens, doesn't issue them).
Authentication: PKCE flow, QuantumID as authority.
Out of scope: social login (Google/GitHub), enterprise SSO (SAML) — future.
[T] TRACE
User calls POST /auth/login → redirect to QuantumID login page
User authenticates at QuantumID → QuantumID issues code
Code → POST /connect/token → QuantumID returns access_token + id_token
Client calls API with Bearer token → API validates against QuantumID JWKS
Token expires → client uses refresh_token → QuantumID issues new access_token
[L] LINK
API → QuantumID: HTTPS, JWKS validation, token introspection
Client → QuantumID: PKCE authorization flow
QuantumID → PostgreSQL (QuantumAPI internal): session storage
[A] ASSEMBLE
1. Register application in QuantumID dashboard
2. Remove JwtService from users-api
3. Add AddAuthentication + AddJwtBearer pointing to QuantumID
4. Configure JWKS URI and token validation parameters
5. Update endpoints: remove /auth/login and /auth/refresh
6. Update UsersController: read sub claim for user identity
[S] STRESS-TEST
JWKS endpoint unreachable?
→ .NET caches JWKS keys — short-term outage handled
Token expired?
→ 401 → client uses refresh_token
Token revoked?
→ Introspection call returns inactive → 401
Paso 1: Registra tu aplicación en QuantumID
Ve a quantumapi.eu, navega a QuantumID → Applications y crea una nueva aplicación:
- Type: Web Application
- Redirect URIs:
https://your-api.example.com/signin-oidc - Post-logout redirect URIs:
https://your-api.example.com/signout-callback-oidc - Allowed scopes:
openid profile email - Grant types: Authorization Code + PKCE
Recibirás un client_id y un client_secret. Guarda el secret en QuantumVault (artículo 2).
Paso 2: Elimina el código JWT casero
Borra JwtService.cs y los endpoints /auth/login y /auth/refresh de la API de usuarios. Ya no son necesarios — QuantumID se encarga de todo esto.
Paso 3: Configura la autenticación en Program.cs
// Program.cs
builder.Services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = "https://id.quantumapi.eu"; // QuantumID authority
options.Audience = "your-client-id"; // From QuantumID dashboard
options.RequireHttpsMetadata = true;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ClockSkew = TimeSpan.FromSeconds(30)
// Algorithm validation: QuantumID enforces ML-DSA — no alg confusion possible
};
});
builder.Services.AddAuthorization();
Eso es todo. El AddJwtBearer de .NET automáticamente:
- Descarga el JWKS desde
https://id.quantumapi.eu/.well-known/jwks.json - Cachea las claves de firma
- Valida los tokens entrantes contra esas claves
- Refresca la caché del JWKS cuando aparecen key IDs desconocidos
Paso 4: Lee la identidad del usuario desde los claims
Antes, JwtService ponía un claim personalizado userId en el token. Ahora, QuantumID pone el ID del usuario en el claim estándar sub:
// UsersController.cs
[HttpGet("{id:guid}")]
[Authorize]
public async Task<IActionResult> GetUser(Guid id)
{
var callerId = User.FindFirstValue(ClaimTypes.NameIdentifier); // = "sub" claim
if (callerId is null) return Unauthorized();
// Ownership check: a user can only read their own record
if (callerId != id.ToString()) return Forbid();
var user = await _users.GetByIdAsync(id);
return user is null ? NotFound() : Ok(user);
}
Paso 5: Token introspection para revocación
Para la mayoría de APIs, la validación JWT es suficiente: valida la firma y la expiración, listo. Pero si necesitas respetar la revocación (un usuario cierra sesión y su token se invalida inmediatamente), necesitas token introspection.
// Add introspection as a fallback validator
builder.Services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = "https://id.quantumapi.eu";
options.Audience = "your-client-id";
options.Events = new JwtBearerEvents
{
OnTokenValidated = async context =>
{
// Only introspect if the token has < 5 minutes left
// to avoid introspecting every request
var expiry = context.SecurityToken.ValidTo;
if (expiry - DateTimeOffset.UtcNow > TimeSpan.FromMinutes(5))
return;
var introspection = context.HttpContext
.RequestServices.GetRequiredService<ITokenIntrospectionService>();
var token = context.Request.Headers.Authorization
.ToString().Replace("Bearer ", "");
var active = await introspection.IsActiveAsync(token);
if (!active)
context.Fail("Token has been revoked");
}
};
});
// TokenIntrospectionService.cs
public class TokenIntrospectionService : ITokenIntrospectionService
{
private readonly HttpClient _http;
public TokenIntrospectionService(HttpClient http)
{
_http = http;
}
public async Task<bool> IsActiveAsync(string token)
{
var response = await _http.PostAsync(
"https://id.quantumapi.eu/connect/introspect",
new FormUrlEncodedContent(new Dictionary<string, string>
{
["token"] = token,
["client_id"] = "your-client-id",
["client_secret"] = Environment.GetEnvironmentVariable("OIDC_CLIENT_SECRET")!
}));
var json = await response.Content.ReadFromJsonAsync<JsonElement>();
return json.GetProperty("active").GetBoolean();
}
}
Para la mayoría de APIs, no necesitas introspection en cada petición — solo para tokens que están a punto de expirar, o para operaciones de alta sensibilidad (pagos, eliminación de cuenta). La validación JWT estándar es suficiente para lecturas normales.
Paso 6: El flujo PKCE para clientes en el navegador
Si tienes un frontend React (como el portal de tenant del proyecto del blog), usa el flujo PKCE:
// auth.ts
import { UserManager, WebStorageStateStore } from 'oidc-client-ts';
export const userManager = new UserManager({
authority: 'https://id.quantumapi.eu',
client_id: 'your-client-id',
redirect_uri: `${window.location.origin}/signin-oidc`,
scope: 'openid profile email',
response_type: 'code',
// PKCE is automatic in oidc-client-ts
userStore: new WebStorageStateStore({ store: window.localStorage }),
});
export const login = () => userManager.signinRedirect();
export const handleCallback = () => userManager.signinRedirectCallback();
export const getAccessToken = async (): Promise<string | null> => {
const user = await userManager.getUser();
if (!user || user.expired) return null;
return user.access_token;
};
El token que devuelve QuantumID está firmado con ML-DSA-65. Tu API lo valida contra el JWKS. El algoritmo de firma no está en la cabecera del token como un campo controlado por el cliente — lo determina la configuración del servidor de QuantumID.
Lo que ajustamos
Dos ajustes después de generar desde el prompt GOTCHA:
1. Caché de JWKS. La IA configuró AddJwtBearer sin un BackchannelHttpHandler y sin establecer RefreshOnIssuerKeyNotFound. La configuración por defecto vuelve a descargar el JWKS en cada key ID desconocido. Añadimos configuración explícita de caché para evitar bombardear el endpoint de QuantumID durante la rotación de claves.
options.BackchannelHttpHandler = new SocketsHttpHandler
{
PooledConnectionLifetime = TimeSpan.FromMinutes(15)
};
options.RefreshOnIssuerKeyNotFound = true; // auto-refresh JWKS on unknown kid
2. El formato del claim sub. QuantumID usa formato UUID para el claim sub (3fa85f64-5717-4562-b3fc-2c963f66afa6). La API de usuarios original usaba Guid.NewGuid().ToString() para los IDs de usuario. Asegúrate de que los IDs de usuario en tu base de datos PostgreSQL coincidan con el formato del claim sub de QuantumID — deberían ser los mismos UUIDs si registraste los usuarios a través de QuantumID.
Template
=== OIDC MIGRATION CHECKLIST ===
PRE-MIGRATION
[ ] Document all custom JWT claims your application uses today
(userId, role, tenantId, etc.)
[ ] Map each custom claim to its OIDC equivalent or plan a custom claim in the provider
QUANTUMID SETUP
[ ] Register application in QuantumID dashboard
[ ] Save client_id and client_secret (client_secret → QuantumVault)
[ ] Configure redirect URIs for all environments (dev, staging, prod)
[ ] Enable PKCE (should be default)
.NET CONFIGURATION
[ ] Replace AddAuthentication + AddJwtBearer with QuantumID authority
[ ] Remove JwtService and custom token generation
[ ] Update claim reading: User.FindFirstValue(ClaimTypes.NameIdentifier) for user ID
[ ] Test token validation with an actual token from QuantumID
ENDPOINTS
[ ] Remove POST /auth/login (handled by QuantumID)
[ ] Remove POST /auth/refresh (handled by QuantumID)
[ ] Keep POST /auth/logout if you need to trigger QuantumID end_session endpoint
INTROSPECTION (optional)
[ ] Decide: do you need immediate revocation?
- Low-sensitivity API: no. Short token expiry (15 min) is enough.
- High-sensitivity operations: yes. Add introspection for those endpoints.
FRONTEND
[ ] Integrate oidc-client-ts or MSAL (if Entra ID is your QuantumID upstream)
[ ] Handle token renewal (silent refresh before expiry)
[ ] Handle 401 responses: trigger re-authentication
Challenge
Ahora tienes:
- Secrets en QuantumVault (artículo 2)
- PII cifrada en la base de datos (artículo 3)
- Autenticación vía QuantumID con tokens ML-DSA (este artículo)
El sistema es seguro en reposo y en la capa de identidad. Pero todavía hay una brecha importante: el pipeline de CI/CD.
Ahora mismo, el pipeline de Azure DevOps de la serie ATLAS+GOTCHA almacena secrets en variable groups. Eso es mejor que YAML, pero sigue siendo cifrado con RSA dentro de Azure Key Vault, y no está gestionado por QuantumVault.
El artículo 5 arregla el pipeline: secrets desde QuantumVault, firma de imágenes con Cosign + claves ML-DSA, y una capa de seguridad de supply chain que va más allá del escaneo con Trivy.
Si esta serie te resulta útil, puedes invitarme a un café.
Loading comments...