Ir al contenido

Validar JWTs en tu backend

Cuando tu app recibe un id_token del callback OIDC (o un access_token en un Authorization: Bearer), tu backend tiene que validarlo antes de confiar en él. Validar significa cuatro cosas:

  1. Firma correcta contra la public key del issuer (JWKS).
  2. iss con sufijo exacto .auth.prysmid.com (formato https://<tu_slug>.auth.prysmid.com).
  3. aud = tu client_id.
  4. exp > ahora.

Si te falta alguna de las cuatro, rechazás. No hay “validar más o menos”.

import { createRemoteJWKSet, jwtVerify } from 'jose';
const ISSUER = 'https://acme.auth.prysmid.com';
const AUDIENCE = process.env.PRYSMID_CLIENT_ID;
// JWKS se cachea automáticamente con TTL razonable y refresca al rotar keys.
const jwks = createRemoteJWKSet(new URL(`${ISSUER}/oauth/v2/keys`));
export async function verifyIdToken(token) {
const { payload } = await jwtVerify(token, jwks, {
issuer: ISSUER,
audience: AUDIENCE,
});
return payload; // { sub, email, name, exp, iat, ... }
}

Cachear JWKS, pero no para siempre. Las keys rotan. Si tu librería cachea JWKS por días sin honrar el Cache-Control o el kid mismatch, vas a empezar a rechazar tokens válidos cuando rotemos. Las librerías arriba (jose, PyJWKClient, go-oidc) lo hacen bien por default.

Reloj. Si tu server tiene clock skew de minutos, vas a rechazar tokens recién emitidos por exp. Sincronizá NTP.

access_token vs id_token. El id_token es para tu backend (probás identidad del usuario). El access_token es para hablar con APIs. No los mezcles.

Multi-tenant: derivá el tenant del token. Si tu app sirve a varios tenants, no alcanza con iss/aud/exp/sig — también tenés que saber a qué tenant corresponde el token para no servir datos de Acme a un user de Globex. PrysmID emite el claim tenant_id explícito (ver sección abajo). Usalo como fuente primaria; si falta, derivá del iss. Lo crítico: que tu lógica de autorización siempre lea el tenant del token validado, nunca de un parámetro de URL o body que el cliente puede mentir.

Además de los claims OIDC estándar (sub, iss, aud, exp, iat, email, name, etc.), PrysmID emite claims propios en el access token y en la respuesta de userinfo. Son tu fuente canónica para identificar tenant y tipo de identidad sin tener que hacer un round-trip al management API.

ClaimTipoDescripción
tenant_idstringSlug del workspace. Igual al subdominio del iss (<tenant_id>.auth.prysmid.com).
workspace_idstring (UUID)ID interno del workspace en PrysmID. Útil para correlation cross-product.
identity_typeenumhuman, machine, service_account, test. Te permite distinguir flows interactivos de M2M de E2E sin lógica externa.
  • iss es el identificador canónico de tenant. Los claims tenant_id y workspace_id son complementarios — facilitan el código pero no reemplazan la validación de iss. Si la Action JS que los emite falla, los claims pueden no aparecer en un token; el iss siempre está.

  • Precedencia obligatoria. Si tenant_id está presente, debe coincidir con el slug derivado del iss. Si difieren, rechazá el token — es señal de manipulación o configuración rota:

    from urllib.parse import urlparse
    def derive_tenant(claims: dict) -> str:
    host = urlparse(claims["iss"]).hostname or ""
    if not host.endswith(".auth.prysmid.com"):
    raise ValueError("issuer not under .auth.prysmid.com")
    slug = host.removesuffix(".auth.prysmid.com")
    claim_tenant = claims.get("tenant_id")
    if claim_tenant and claim_tenant != slug:
    raise ValueError("tenant_id claim conflicts with iss")
    return claim_tenant or slug
  • identity_type para autorización fina. Un caso típico: tu API permite a human y service_account pero no a test; o aplica rate-limits distintos por tipo. Lecturarlo del claim evita que tengas que mantener una tabla propia de “este sub es bot”.

Las service accounts (crear / listar / revocar) emiten tokens vía el JWT-bearer profile (RFC 7523) contra /oauth/v2/token. Esos tokens son JWTs RS256 firmados por la misma JWKS que los tokens humanos — mismo verificador, mismos claims OIDC, más:

  • identity_type: "service_account" (en lugar de human).
  • sub = ID del machine user (estable).
  • aud puede ser narrow (client_id específico) o el general de la instance, según cómo configures la SA.

No necesitás un verificador distinto. La única diferencia operativa es que no hay refresh token en el flujo M2M: la SA firma una nueva assertion cada vez que necesita un access token.

ErrorCausa típica
Invalid signatureCacheaste una JWKS vieja, o tu librería no soporta RS256 (chequeá flags).
Invalid audienceTu client_id cambió, o el token vino de otra app de tu workspace.
Token expiredEl id_token dura ~1h. Refrescá con refresh_token.
Issuer mismatchCambiaste el slug del workspace. El iss lo refleja: <slug>.auth.prysmid.com.