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.
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) ) )
- and _).= padding.SmartAuth.codeChallenge("dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk");
// -> "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"
Use MessageDigest.getInstance("SHA-256") and
Base64.getUrlEncoder().withoutPadding().
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.
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:
context matches exactly, and
resource equals the required resource or is *, and
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
Sign up to Exercism to learn and master FHIR with 9 exercises, and real human mentoring, all for free.