JWT-AT claim set (RFC 9068)
The shape
REQUIRED claims (per RFC 9068 §2.2): iss=https://as.oid4pay.com, exp, aud (the resource URL the AT is bound to; see resource indicators), sub (principal_id), client_id, iat, jti. RECOMMENDED: auth_time, acr, amr, scope. For DPoP binding (RFC 9449 §6): cnf.jkt matching the agent's DPoP key thumbprint.
REQUIRED header: typ=at+jwt (RFC 9068 §2.1). This prevents ID-token-as-AT confusion
attacks.
AT TTL: 300 seconds (5 minutes). Short-lived to limit leak window; refresh tokens cover the UX gap.
Header
{
"typ": "at+jwt",
"alg": "EdDSA",
"kid": "as-2026-05-14"
}Claim set
{
"iss": "https://as.oid4pay.com",
"sub": "principal_id_b64url",
"aud": "https://shop.alpacanica.com",
"client_id": "client_abc",
"jti": "01HJ9XK0YN0K6V6S8Y8E5P5W6Y",
"exp": 1747260600,
"iat": 1747260300,
"nbf": 1747260300,
"scope": "oid4ac:payment",
"auth_time": 1747260280,
"acr": "urn:oid4pay:webauthn:2fa",
"amr": ["pwd", "webauthn"],
"cnf": { "jkt": "base64url-sha256-of-dpop-public-jwk" },
"mandate_id": "mandate_xyz",
"agent_client_id": "client_abc"
}Verifier checklist
Every protected endpoint MUST validate:
- Header
typ=at+jwtexactly. - Signature against the AS JWKS at
https://as.oid4pay.com/oauth/jwks.json. iss=https://as.oid4pay.comexactly.audmatches the RS's own resource URL exactly.expin the future.cnf.jktmatches the JWK thumbprint of the DPoP proof on the same request.scopeincludes a value the RS recognises.
Worked example
// Node merchant SDK verifier (under the hood):
import { importJWK, jwtVerify } from "jose";
import { calculateJwkThumbprint } from "jose";
async function verifyJwtAt(token, dpopJwk, expectedAudience) {
const jwks = await fetchAsJwks();
const { payload, protectedHeader } = await jwtVerify(token, jwks, {
issuer: "https://as.oid4pay.com",
audience: expectedAudience,
typ: "at+jwt",
algorithms: ["EdDSA"],
});
const dpopJkt = await calculateJwkThumbprint(dpopJwk, "sha256");
if (payload.cnf?.jkt !== dpopJkt) {
throw new Error("dpop_binding_mismatch");
}
return payload;
}Why typ pinning matters
Without typ=at+jwt, a verifier that accepts an ID token by mistake would let an
attacker present a long-lived id_token as an access token. Pinning typ in the header
rejects the wrong-class token before signature verification.