Resolve documents
Documents are not in the payload as bytes — each descriptor carries an id and a resolveUrl. To get
the bytes, you call the Consumer, authenticating with a signed JWT assertion (the same asymmetric
key you publish at jwks_uri):
sequenceDiagram
autonumber
participant P as You (Provider)
participant C as Consumer
participant S as Object storage
P->>C: POST resolveUrl with id and signed JWT assertion
C->>C: verify JWT against your jwks_uri, map iss to connection, check scan-clean and retention
C-->>P: returns short-lived signed url, expiresAt, sha256
P->>S: GET the signed url
S-->>P: bytes
P->>P: verify sha256, then store
The assertion is a private_key_jwt (RFC 7523): iss is your
issuer (matching discovery), aud is the descriptor’s resolveUrl, plus jti, iat, and a short exp.
Sign it with the key you publish at jwks_uri. Using jose (works on
Node and edge runtimes):
import { SignJWT, importPKCS8 } from "jose"
// PROVIDER_PRIVATE_KEY_PEM corresponds to a public key published at your jwks_uri.const privateKey = await importPKCS8(PROVIDER_PRIVATE_KEY_PEM, "RS256")
async function resolveDocument(resolveUrl: string, id: string) { const assertion = await new SignJWT({}) .setProtectedHeader({ alg: "RS256", kid: PROVIDER_SIGNING_KID }) .setIssuer("https://sms.example.edu") // matches discovery `issuer` .setAudience(resolveUrl) // the descriptor's resolveUrl — bind the token .setJti(crypto.randomUUID()) .setIssuedAt() .setExpirationTime("60s") .sign(privateKey)
const res = await fetch(resolveUrl, { method: "POST", headers: { authorization: `Bearer ${assertion}`, "content-type": "application/json" }, body: JSON.stringify({ id }), }) if (res.status === 410) throw new Error("document expired (past retention)") if (!res.ok) throw new Error(`resolve failed: ${res.status}`) return res.json() as Promise<{ url: string; expiresAt: string; sha256: string }>}The Consumer returns { "url": "...", "expiresAt": "...", "sha256": "..." }. Then:
- Download promptly — the
urlis short-lived and may be single-use. - Verify the
sha256of the bytes before storing. - Handle
410 Gone(past the Consumer’s retention window) and409(scan still pending) gracefully.
Why a signed assertion and not a pasted secret: it keeps every secret out of the payload, and it reuses the
key you already publish at jwks_uri — the Consumer already knows that key from your discovery document, so
no extra credential exchange is needed.