Tracks
/
FHIR
FHIR
/
Exercises
/
SMART on FHIR Token Flow (Java)
SMART on FHIR Token Flow (Java)

SMART on FHIR Token Flow (Java)

Medium

Instructions

SMART on FHIR apps use OAuth 2.0's authorization-code flow with PKCE to obtain an access token that is scoped to exactly the FHIR data the app is allowed to touch. This kata is the offline, deterministic version of Batch-1 Day 6/7, Week 2 Assignments 5 & 7 (Keycloak JWT / SMART on FHIR auth): before you can wire a real authorization server, you must be able to reproduce the three pieces of pure logic the flow depends on.

Implement SmartAuth with three static methods.

1. PKCE code challenge (S256)

PKCE ("Proof Key for Code Exchange") stops an intercepted authorization code from being redeemed by an attacker. The client invents a random high-entropy string, the code verifier, and sends only a hashed form of it β€” the code challenge β€” when it starts the flow. At token time it reveals the verifier; the server re-hashes it and checks it matches.

The S256 method is:

code_challenge = BASE64URL-WITHOUT-PADDING( SHA-256( ASCII(code_verifier) ) )
  • Hash the UTF-8 bytes of the verifier with SHA-256.
  • Base64url-encode the 32-byte digest (URL-safe alphabet, - and _).
  • Strip the trailing = padding.
SmartAuth.codeChallenge("dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk");
// -> "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"

Use MessageDigest.getInstance("SHA-256") and Base64.getUrlEncoder().withoutPadding().

2. Authorize URL

Build the URL the browser is redirected to when launching the app. Append a query string to authorizeEndpoint with the parameters in exactly this order, each value URL-encoded with URLEncoder.encode(value, "UTF-8"), joined by &:

response_type=code
client_id=<clientId>
redirect_uri=<redirectUri>
scope=<scope>
state=<state>
aud=<aud>
code_challenge=<codeChallenge>
code_challenge_method=S256

The result is authorizeEndpoint + "?" + query. The endpoint itself is not encoded.

3. Scope matching

SMART v1 scopes have the form <context>/<resource>.<access>:

  • context β€” patient, user, or system.
  • resource β€” a FHIR resource type (e.g. Observation) or * for any.
  • access β€” read, write, or * for any.

scopeGrants(grantedScopes, requiredScope) takes a space-separated list of granted scopes and a single required scope, and returns true iff some granted scope grants the required one. A granted scope grants the required scope iff:

  • the context matches exactly, and
  • the granted resource equals the required resource or is *, and
  • the granted access equals the required access or is *.
SmartAuth.scopeGrants("patient/*.read", "patient/Observation.read"); // true
SmartAuth.scopeGrants("user/*.*",       "user/Encounter.write");     // true
SmartAuth.scopeGrants("patient/Observation.read", "patient/Patient.read"); // false

Source

Batch-1 Day 6/7 Week 2 Assignments 5 & 7 (Keycloak JWT / SMART on FHIR auth), refactored to an offline SMART OAuth2 + PKCE logic kata.
Edit via GitHub The link opens in a new window or tab
FHIR Exercism

Ready to start SMART on FHIR Token Flow (Java)?

Sign up to Exercism to learn and master FHIR with 9 exercises, and real human mentoring, all for free.