SMART on FHIR
This guide explains how to use Descope as an OAuth 2.0 provider for SMART on FHIR applications. SMART on FHIR is a healthcare industry standard that allows third-party applications to securely access Electronic Health Record (EHR) data.
Descope's Inbound Apps feature enables you to act as an OAuth provider for SMART apps, handling user authentication, consent, and token issuance while your application manages the SMART-specific launch context and FHIR API calls.
Overview
SMART on FHIR provides a standardized way for healthcare applications to:
- Launch from within an EHR system (EHR Launch) or as a standalone application (Standalone Launch)
- Request access to patient data with granular scopes
- Handle user authentication and authorization
- Access FHIR resources on behalf of authenticated users
Descope supports both EHR Launch and Standalone Launch flows through Inbound Apps, which provide:
/authorizeendpoint for user authentication and consent/tokenendpoint for token exchange- JWT templates for customizing access tokens with SMART-specific claims
- Scope management and validation
Architecture
In a SMART on FHIR integration with Descope:
- Your application receives launch requests from the EHR (for EHR Launch) or initiates authentication (for Standalone Launch)
- Descope acts as the OAuth authorization server, handling user authentication and consent
- Your application receives the access token and uses it to call the EHR's FHIR API
The access token issued by Descope includes SMART-specific claims (like patient, encounter) that your application forwards to the EHR's FHIR server.
Launch Types
EHR Launch
The app is launched from within the EHR. The EHR redirects to your app with a launch parameter containing an encrypted launch context token.
Flow:
- User clicks your app in the EHR
- EHR redirects to your app with
iss(EHR issuer URL) andlaunchparameters - Your app extracts launch context and redirects to Descope for authorization
- User authenticates and consents via Descope
- Descope redirects back with authorization code
- Your app exchanges code for access token
- Access token includes launch context (patient ID, encounter ID, etc.)
Standalone Launch
The app is launched independently, without being embedded in the EHR. Users authenticate directly.
Flow:
- User navigates directly to your app
- Your app redirects to Descope for authorization
- User authenticates and consents via Descope
- Descope redirects back with authorization code
- Your app exchanges code for access token
- Access token can be used to access FHIR resources (patient selection may happen later)
Integration Steps
1. Configure Your Inbound App in Descope
-
In the Descope Console, navigate to Inbound Apps. Click + Inbound App.
-
Configure the required scopes for your SMART app. Common SMART scopes include:
patient/*.rs- Read and search any resource for the current patientpatient/*.read- Read any resource for the current patientuser/*.rs- Read and search any resource for the current userlaunch- Required for EHR Launch to receive launch contextopenid- Standard OIDC scopefhirUser- Retrieve information about the current logged-in useroffline_access- Request a refresh token
For a complete list of SMART scopes, refer to the SMART App Launch documentation.
-
Set the redirect URI to your application's callback URL (e.g.,
https://yourapp.com/oauth/callback) -
Optionally customize your consent flow to match your branding
2. Configure JWT Template for SMART Claims
Create a JWT template that includes SMART-specific claims in the access token:
- In the Descope Console, go to Project Settings -> JWT Templates, and create a new template.
- Configure the template with SMART-specific claims:
{
"aud": "{{fhir_server_url}}",
"scope": "{{scopes}}",
"patient": "{{patient_id}}",
"encounter": "{{encounter_id}}",
"need_patient_banner": true,
"smart_style_url": "{{smart_style_url}}"
}Some of the key claims that you will need to configure are:
aud- The FHIR server URL (audience). This should match the EHR's FHIR endpointscope- The authorized scopes (automatically included)patient- Patient ID from launch context (for EHR Launch)encounter- Encounter ID from launch context (if available)need_patient_banner- Indicates if the app should display patient contextsmart_style_url- URL for SMART styling resources (optional)
- Under Project Settings -> Session Management, assign this JWT template as the User JWT
The aud claim must match the FHIR server URL where your app will make API calls. For multi-EHR scenarios, you may need to use dynamic values or create separate templates per EHR.
3. Handle Launch Parameters (EHR Launch)
When your app receives a launch request from the EHR, extract the launch context:
// Example: Express.js route handler
app.get('/launch', (req, res) => {
const { iss, launch } = req.query;
// Store launch context temporarily (e.g., in session or encrypted cookie)
// You'll need to decode the launch parameter to extract patient/encounter IDs
const launchContext = {
iss, // EHR issuer URL
launch, // Launch context token
state: generateRandomState()
};
// Store in session for later use
req.session.launchContext = launchContext;
// Redirect to Descope authorization
const authUrl = buildAuthorizationUrl(launchContext);
res.redirect(authUrl);
});4. Build Authorization Request
Construct the authorization request to Descope's /authorize endpoint:
function buildAuthorizationUrl(launchContext: LaunchContext): string {
const params = new URLSearchParams({
client_id: process.env.DESCOPE_CLIENT_ID!,
redirect_uri: 'https://yourapp.com/oauth/callback',
response_type: 'code',
scope: 'openid fhirUser patient/*.read launch',
state: launchContext.state,
// Include launch parameter for EHR Launch
...(launchContext.launch && { launch: launchContext.launch }),
// Include iss parameter
...(launchContext.iss && { iss: launchContext.iss })
});
return `https://api.descope.com/oauth2/v1/apps/authorize?${params.toString()}`;
}5. Handle OAuth Callback
After user authentication and consent, Descope redirects back to your callback URL:
app.get('/oauth/callback', async (req, res) => {
const { code, state } = req.query;
// Verify state matches
if (state !== req.session.launchContext?.state) {
return res.status(400).send('Invalid state parameter');
}
// Exchange authorization code for access token
const tokenResponse = await exchangeCodeForToken(code as string);
// Extract launch context from token (if EHR Launch)
const launchContext = extractLaunchContext(tokenResponse);
// Store token and launch context
req.session.accessToken = tokenResponse.access_token;
req.session.patientId = launchContext.patient;
req.session.encounterId = launchContext.encounter;
// Redirect to your app's main interface
res.redirect('/app');
});6. Exchange Authorization Code for Token
Note
The client_secret comes from the Inbound App configuration.
Exchange the authorization code for an access token:
async function exchangeCodeForToken(code: string) {
const response = await fetch('https://api.descope.com/oauth2/v1/apps/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'authorization_code',
client_id: process.env.DESCOPE_CLIENT_ID!,
client_secret: process.env.DESCOPE_CLIENT_SECRET!,
code: code,
redirect_uri: 'https://yourapp.com/oauth/callback',
}),
});
return await response.json();
}The token response includes:
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "abc123...",
"scope": "openid fhirUser patient/*.read launch",
"id_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
}7. Extract Launch Context from Token
Decode the access token to extract SMART-specific claims:
import jwt from 'jsonwebtoken';
function extractLaunchContext(tokenResponse: TokenResponse) {
const decoded = jwt.decode(tokenResponse.access_token, { complete: true });
const payload = decoded?.payload as any;
return {
patient: payload.patient,
encounter: payload.encounter,
need_patient_banner: payload.need_patient_banner,
smart_style_url: payload.smart_style_url,
};
}8. Use Access Token with FHIR API
Use the access token to call the EHR's FHIR API:
async function fetchPatientData(accessToken: string, patientId: string) {
const fhirServerUrl = process.env.FHIR_SERVER_URL!;
const response = await fetch(
`${fhirServerUrl}/Patient/${patientId}`,
{
headers: {
'Authorization': `Bearer ${accessToken}`,
'Accept': 'application/fhir+json',
},
}
);
return await response.json();
}Standalone Launch Example
For Standalone Launch, the flow is simpler since there's no launch context:
// User navigates directly to your app
app.get('/login', (req, res) => {
const authUrl = new URL('https://api.descope.com/oauth2/v1/apps/authorize');
authUrl.searchParams.set('client_id', process.env.DESCOPE_CLIENT_ID!);
authUrl.searchParams.set('redirect_uri', 'https://yourapp.com/oauth/callback');
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('scope', 'openid fhirUser patient/*.read');
authUrl.searchParams.set('state', generateRandomState());
res.redirect(authUrl.toString());
});
// After authentication, user may need to select a patient
// This is typically done through a patient picker interfaceHandling Launch Context
For EHR Launch, you need to decode the launch parameter to extract patient and encounter IDs. The launch parameter is typically a JWT or encrypted token provided by the EHR.
The exact format of the launch parameter varies by EHR. Some EHRs provide it as a simple identifier that you exchange for launch context via their API, while others provide it as a JWT. Consult your EHR's SMART on FHIR documentation for specifics.
Token Refresh
SMART on FHIR access tokens are typically short-lived. Use the refresh token to obtain a new access token:
async function refreshAccessToken(refreshToken: string) {
const response = await fetch('https://api.descope.com/oauth2/v1/apps/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'refresh_token',
client_id: process.env.DESCOPE_CLIENT_ID!,
client_secret: process.env.DESCOPE_CLIENT_SECRET!,
refresh_token: refreshToken,
}),
});
return await response.json();
}Troubleshooting
These are some common issues you might encounter when integrating with SMART on FHIR:
| Issue | Likely Cause | Solution |
|---|---|---|
Invalid launch parameter | Launch context expired or malformed | Ensure launch context is used immediately after receipt |
Missing patient claim in token | Launch context not properly extracted | Verify launch parameter decoding and JWT template configuration |
| FHIR API returns 401 | aud claim doesn't match FHIR server URL | Ensure JWT template aud claim matches the EHR's FHIR endpoint |
| Scope not granted | Scope not configured in Inbound App | Add required scope to Inbound App configuration |
| State mismatch | CSRF protection or session issue | Verify state parameter is properly stored and validated |