Is PKCE required for SMART on FHIR?
SMART App Launch 2.2 requires apps to support PKCE. Servers validate the code verifier during token exchange. Use the S256 challenge method.
Production-grade access is free with an account. · Synthetic demo · no account needed
Patient-authorized EHR integration
A security-first walkthrough of discovery, authorization, callback validation, and token exchange for a standalone patient-facing application.
Updated: 2026-08-25 · 13 min read
A SMART standalone launch is an OAuth 2.0 authorization-code flow with FHIR-specific discovery, audience, scopes, and patient context. Generate PKCE and state on the server, send only the authorization URL to React, and exchange the returned code from a trusted backend.
Evidence boundary: Protocol behavior is grounded in SMART App Launch 2.2. The code is educational and uses placeholder endpoints rather than a live EHR.
Author: FinchNode Engineering
Read the server’s SMART configuration and select its authorize and token endpoints.
Generate unpredictable state and a PKCE verifier; store them with a short expiry.
Request an authorization code with the registered redirect URI, FHIR audience, minimum scopes, and S256 challenge.
On callback, reject missing, expired, reused, or mismatched state before exchanging the code.
Send the code, verifier, client identifier, and exact redirect URI to the token endpoint.
Validate the response and associate the returned patient context with the authorized source connection.
The verifier must remain secret until the token exchange. Store only the transaction identifier in the browser session or secure, same-site cookie.
import { createHash, randomBytes } from 'node:crypto';
const base64url = (value) => value.toString('base64url');
const state = base64url(randomBytes(32));
const verifier = base64url(randomBytes(64));
const challenge = base64url(createHash('sha256').update(verifier).digest());
await transactions.save({ state, verifier, expiresAt: Date.now() + 5 * 60_000 });
const authorize = new URL(smart.authorize_endpoint);
authorize.searchParams.set('response_type', 'code');
authorize.searchParams.set('client_id', process.env.FHIR_CLIENT_ID);
authorize.searchParams.set('redirect_uri', 'https://app.example.com/auth/callback');
authorize.searchParams.set('aud', fhirBaseUrl);
authorize.searchParams.set('scope', 'openid fhirUser launch/patient patient/*.rs');
authorize.searchParams.set('state', state);
authorize.searchParams.set('code_challenge', challenge);
authorize.searchParams.set('code_challenge_method', 'S256');
return { authorizationUrl: authorize.toString() };
Use the exact redirect URI registered with the server. Consume state once so a valid callback cannot be replayed. Do not log authorization codes or token responses.
const transaction = await transactions.consume(request.query.state);
if (!transaction || transaction.expiresAt < Date.now()) throw new Error('invalid_state');
const body = new URLSearchParams({
grant_type: 'authorization_code',
code: request.query.code,
client_id: process.env.FHIR_CLIENT_ID,
redirect_uri: 'https://app.example.com/auth/callback',
code_verifier: transaction.verifier,
});
const tokenResponse = await fetch(smart.token_endpoint, {
method: 'POST',
headers: { 'content-type': 'application/x-www-form-urlencoded' },
body,
});
if (!tokenResponse.ok) throw new Error('token_exchange_failed');
const token = await tokenResponse.json();
SMART App Launch 2.2 requires apps to support PKCE. Servers validate the code verifier during token exchange. Use the S256 challenge method.
A browser-only public client can implement PKCE, but applications with a backend should generally keep token exchange and persistent tokens on the trusted server so tokens are not exposed to browser storage or application JavaScript.
It identifies the FHIR resource server the application intends to access. Use the server’s advertised FHIR base URL and follow the implementation guide and vendor registration requirements.
For a standalone patient launch, the authorization server can return a patient identifier in the token response when the granted launch context includes a patient.