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:
- Firma correcta contra la public key del issuer (JWKS).
isscon sufijo exacto.auth.prysmid.com(formatohttps://<tu_slug>.auth.prysmid.com).aud= tuclient_id.exp> ahora.
Si te falta alguna de las cuatro, rechazás. No hay “validar más o menos”.
Ejemplos
Sección titulada «Ejemplos»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, ... }}import jwtfrom jwt import PyJWKClient
ISSUER = "https://acme.auth.prysmid.com"AUDIENCE = os.environ["PRYSMID_CLIENT_ID"]
jwks_client = PyJWKClient(f"{ISSUER}/oauth/v2/keys")
def verify_id_token(token: str) -> dict: signing_key = jwks_client.get_signing_key_from_jwt(token) return jwt.decode( token, signing_key.key, algorithms=["RS256"], audience=AUDIENCE, issuer=ISSUER, )import ( "context" "github.com/coreos/go-oidc/v3/oidc")
const issuer = "https://acme.auth.prysmid.com"
func newVerifier(ctx context.Context) (*oidc.IDTokenVerifier, error) { provider, err := oidc.NewProvider(ctx, issuer) if err != nil { return nil, err } return provider.Verifier(&oidc.Config{ ClientID: os.Getenv("PRYSMID_CLIENT_ID"), }), nil}
func verify(ctx context.Context, v *oidc.IDTokenVerifier, raw string) (*oidc.IDToken, error) { return v.Verify(ctx, raw)}require 'jwt'require 'open-uri'require 'json'
ISSUER = 'https://acme.auth.prysmid.com'AUDIENCE = ENV['PRYSMID_CLIENT_ID']
def jwks @jwks ||= JSON.parse(URI.open("#{ISSUER}/oauth/v2/keys").read)end
def verify_id_token(token) decoded, = JWT.decode(token, nil, true, { algorithms: ['RS256'], iss: ISSUER, verify_iss: true, aud: AUDIENCE, verify_aud: true, jwks: jwks, }) decodedendCosas que la mayoría olvida
Sección titulada «Cosas que la mayoría olvida»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.
Claims PrysmID en el token
Sección titulada «Claims PrysmID en el token»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.
| Claim | Tipo | Descripción |
|---|---|---|
tenant_id | string | Slug del workspace. Igual al subdominio del iss (<tenant_id>.auth.prysmid.com). |
workspace_id | string (UUID) | ID interno del workspace en PrysmID. Útil para correlation cross-product. |
identity_type | enum | human, machine, service_account, test. Te permite distinguir flows interactivos de M2M de E2E sin lógica externa. |
Contrato de los claims
Sección titulada «Contrato de los claims»-
isses el identificador canónico de tenant. Los claimstenant_idyworkspace_idson complementarios — facilitan el código pero no reemplazan la validación deiss. Si la Action JS que los emite falla, los claims pueden no aparecer en un token; elisssiempre está. -
Precedencia obligatoria. Si
tenant_idestá presente, debe coincidir con el slug derivado deliss. Si difieren, rechazá el token — es señal de manipulación o configuración rota:from urllib.parse import urlparsedef 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_typepara autorización fina. Un caso típico: tu API permite ahumanyservice_accountpero no atest; o aplica rate-limits distintos por tipo. Lecturarlo del claim evita que tengas que mantener una tabla propia de “este sub es bot”.
Tokens de service accounts (M2M)
Sección titulada «Tokens de service accounts (M2M)»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 dehuman).sub= ID del machine user (estable).audpuede ser narrow (client_idespecí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.
Errores comunes y qué significan
Sección titulada «Errores comunes y qué significan»| Error | Causa típica |
|---|---|
Invalid signature | Cacheaste una JWKS vieja, o tu librería no soporta RS256 (chequeá flags). |
Invalid audience | Tu client_id cambió, o el token vino de otra app de tu workspace. |
Token expired | El id_token dura ~1h. Refrescá con refresh_token. |
Issuer mismatch | Cambiaste el slug del workspace. El iss lo refleja: <slug>.auth.prysmid.com. |