Skip to content

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 url is short-lived and may be single-use.
  • Verify the sha256 of the bytes before storing.
  • Handle 410 Gone (past the Consumer’s retention window) and 409 (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.