JWT Authorization — QIDO-RS / WADO-RS
The QIDO-RS and WADO-RS read interfaces support study-scoped access with
Authorization: Bearer viewer JWTs. Authorization is off by default: it is
enabled per deployment by configuring a token-verification key. When enabled,
every QIDO-RS / WADO-RS request must carry a valid viewer token and may only
reach studies named in that token.
This page describes the mechanism from both a client-implementer and an operator perspective.
Endpoints covered
| Endpoints | Authorization |
|---|---|
QIDO-RS — GET /studies, /studies/{study}/series |
Viewer JWT required when enabled |
WADO-RS — all GET retrieve endpoints (study, series, instance, frames, metadata, rendered, thumbnail) |
Viewer JWT required when enabled |
STOW-RS — POST /studies, POST /studies/{study} |
Never gated — ingestion authenticates out-of-band (mTLS proxy) |
The viewer token
The viewer JWT is minted by the smart-launch-api service at the end of a SMART on FHIR EHR launch flow. The DICOMweb service holds only the RS256 public half of the signing keypair and verifies tokens offline against it.
Verification is strict. A token must:
- be signed with RS256 (
algmust be RS256) - carry the expected
iss(issuer) claim - carry the expected
aud(audience) claim - not be expired (
expis enforced with no clock skew)
| Claim | Requirement | Purpose |
|---|---|---|
iss |
Must match the configured issuer | Names the trusted token service |
aud |
Must match the configured audience | Names this service |
exp |
Must be in the future | Expiry, enforced strictly |
sub |
Present | User identifier — audit attribution only |
ehrIss |
Present | EHR issuer that namespaced sub — audit attribution only |
studyInstanceUIDs |
Array of Study Instance UIDs | The authorization scope |
sub and ehrIss take part in no authorization decision. The scope is the
studyInstanceUIDs array; see below.
When enabled in the DICOMweb service, requests to the QIDO-RS or WADO-RS
services must include a valid signed JWT token. This token shall be provided
by including an Authorization header in the standard way:
Authorization: Bearer <jwt-token>
Study scope
The token's studyInstanceUIDs claim lists the Study Instance UIDs the holder
may reach:
- WADO-RS and series search — every endpoint whose path names a Study
Instance UID (
/studies/{study}/...) requires that study to appear in the token's scope. A valid token naming a different study is refused with403 (Forbidden). - Study search (
/studies) — results are intersected with the token's scope: only studies the token grants can be returned. - Wildcard — a scope containing the literal
'*'grants every study. The token is still required, but no per-study restriction applies. - Empty scope — grants nothing. Study search returns an empty array, and
direct study access is refused with
403. An empty scope is an authorization outcome, not an error.
Status codes
When authorization is enabled:
| Code | Meaning | Response body |
|---|---|---|
401 (Unauthorized) |
No Authorization: Bearer header, a malformed one, or a token that is invalid or expired (bad signature, wrong iss/aud, wrong algorithm, past exp) |
{"error": "Unauthorized"} |
403 (Forbidden) |
A valid token that does not grant the requested study | {"error": "Study not in scope"} |
A malformed Authorization header — missing token, extra segments — is
rejected rather than salvaged.
Examples
Authenticated request
curl "http://localhost:3000/studies?PatientID=PATIENT001" \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9. ..." \
-H "Accept: application/dicom+json"
curl "http://localhost:3000/studies/1.2.840.../series/1.2.840.../instances/1.2.840.../metadata" \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9. ..." \
-H "Accept: application/dicom+json"
Missing or invalid token
HTTP/1.1 401 Unauthorized
Content-Type: application/json
{"error":"Unauthorized"}
A client receiving 401 should obtain a fresh token from the launch flow and
retry once; persistent 401s indicate a misconfigured issuer, audience, or
signing keypair.
Study outside the token's scope
HTTP/1.1 403 Forbidden
Content-Type: application/json
{"error":"Study not in scope"}
A 403 means the token itself is fine — the client must request a token whose
studyInstanceUIDs includes the study it wants.
Study search under a scoped token
A study search with a valid token returns only studies the token grants, and is indistinguishable in shape from an unscoped search:
curl "http://localhost:3000/studies?StudyDate=20240101-20240131" \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9. ..." \
-H "Accept: application/dicom+json"
Studies the token does not grant are silently excluded. The Warning header
semantics described in QIDO-RS apply as normal.
Client implementer notes
- Send the header on every read request. There is no session or cookie fallback.
- Never place the token in a URL. It is a header value only; URLs reach access logs, proxy logs, and browser history.
- Treat
401and403differently.401asks for a new token;403asks for a different token (one whose scope names the study). - Request tokens with the narrowest useful scope. The scope is the set of studies the session may open; a wildcard token is only needed when the session genuinely browses all studies.
Operating the feature
| Variable | Required | Default | Description |
|---|---|---|---|
JWT_VERIFYING_KEY |
No | — (feature off) | RS256 PEM public key matching the token service's signing key. Its presence turns verification on. \n escape sequences are normalized to real newlines, so both dotenv multi-line and single-line escaped forms work. A value that is not a valid RSA public key — garbage, a private key pasted by mistake, or a key of another algorithm (e.g. EC) — fails startup. The private key must never reach this service. |
JWT_ISSUER |
No | kastoria-smart-launch |
Expected iss claim; a token with any other issuer is rejected with 401 |
JWT_AUDIENCE |
No | kastoria-health |
Expected aud claim; a token with any other audience is rejected with 401 |
- Key distribution is a deployment concern. The keypair is generated and
managed alongside the token service's
JWT_SIGNING_KEY; the operator places the same keypair's public half in this service and its private half in the token service. Startup fails fast on a misconfigured key so a bad deploy surfaces immediately rather than as the first user's401. - With the key unset the feature is off — no header is required and no scope is enforced, and a warning is logged at startup saying so.
- Tokens are PHI-adjacent and are never logged. The service records only
that a request was authenticated and the configured
iss/audconstants; scope denials are logged generically without naming the study or the user. Clients must keep tokens out of their own logs for the same reason.
See also
- QIDO-RS — Search for Instances
- WADO-RS — Retrieve Instances
- STOW-RS — Store Instances — exempt from viewer-token authorization by design