Skip to content

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 (alg must be RS256)
  • carry the expected iss (issuer) claim
  • carry the expected aud (audience) claim
  • not be expired (exp is 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 with 403 (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 401 and 403 differently. 401 asks for a new token; 403 asks 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's 401.
  • 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/aud constants; 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