{"id":73460,"date":"2025-07-25T16:14:40","date_gmt":"2025-07-25T10:44:40","guid":{"rendered":"https:\/\/www.tothenew.com\/blog\/?p=73460"},"modified":"2025-07-30T13:18:20","modified_gmt":"2025-07-30T07:48:20","slug":"implementing-configurable-smart-on-fhir-authentication-with-node-js","status":"publish","type":"post","link":"https:\/\/www.tothenew.com\/blog\/implementing-configurable-smart-on-fhir-authentication-with-node-js\/","title":{"rendered":"Implementing configurable SMART on FHIR authentication with Node.js"},"content":{"rendered":"<h2>Introduction<\/h2>\n<p>In the rapidly evolving landscape of health-tech, interoperability and security are critical. SMART on FHIR (Substitutable Medical Applications, Reusable Technologies on Fast Healthcare Interoperability Resources) has emerged as a robust standard to create apps that integrate seamlessly with Electronic Health Record systems.<\/p>\n<p>This blog, which is meant for developers building healthcare apps, covered the full lifecycle of implementing a configurable SMART on FHIR authentication system using Node.js and Express, with support for both Epic and Cerner EHR.<\/p>\n<h2>What is FHIR?<\/h2>\n<p>FHIR (pronounced as fire) is a standard developed by HL7 to simplify the exchange of healthcare data between different systems and applications<\/p>\n<h3>Key features:<\/h3>\n<ul>\n<li>Breaks healthcare data into small, modular components called resources (e.g Patient, Encounter)<\/li>\n<li>Defines standard structure, content, and semantics for these.<\/li>\n<li>Exposes healthcare resources via RESTful APIs.<\/li>\n<\/ul>\n<p>Example of simple Patient resource:<\/p>\n<pre>{\r\n \u00a0\"resourceType\": \"Patient\",\r\n \u00a0\"id\": \"example\",\r\n \u00a0\"name\": [{\"family\": \"Sheikh\", \"given\": [\"Mavin\"]}],\r\n \u00a0\"gender\": \"male\",\r\n \u00a0\"birthDate\": \"1980-26-26\"\r\n}<\/pre>\n<h2>What is SMART on FHIR?<\/h2>\n<p>SMART on FHIR is an open standard that allows third-party apps to connect with healthcare systems using FHIR Rest API\u2019s and standardised OAuth 2.0 authorization flows.<\/p>\n<h2>SMART App Launch Types<\/h2>\n<p>SMART on FHIR defines three ways to launch the app:<\/p>\n<ul>\n<li><strong>Standalone Launch<\/strong>: Launched directly by the user outside the EHR.<\/li>\n<li><strong>EHR (also called as Embedded) Launch<\/strong>: Launched within an EHR application.<\/li>\n<li><strong>Backend systems<\/strong>: For Apps without direct end user or patient interaction (<em>In this blog we will only discuss first two launch types)<\/em><\/li>\n<\/ul>\n<h2>Standalone Launch Sequence<\/h2>\n<ul>\n<li>Register the App with the FHIR server to get a Client ID (e.g, via <a href=\"https:\/\/fhir.epic.com\/Developer\/Apps\" rel=\"nofollow\">Epic<\/a> or <a href=\"https:\/\/code-console.cerner.com\/console\/apps\" rel=\"nofollow\">Cerner<\/a>).<\/li>\n<li>The user visits your app directly outside of EHR.<\/li>\n<li>Get SMART well known configuration from the FHIR server (also called as conformance statement).<\/li>\n<li>Redirect user to the FHIR server\u2019s authorize endpoint.<\/li>\n<li>FHIR server authenticates user and redirects to your callback url with an authorization code.<\/li>\n<li>Exchange the code at the token endpoint to get access token.<\/li>\n<\/ul>\n<div id=\"attachment_73056\" class=\"wp-caption alignnone\" style=\"width: 802px;\">\n<p><img decoding=\"async\" loading=\"lazy\" class=\"size-full wp-image-73056\" src=\"https:\/\/www.tothenew.com\/blog\/wp-ttn-blog\/uploads\/2025\/07\/Screenshot-from-2025-07-03-06-22-16.png\" alt=\"Standalone launch\" width=\"792\" height=\"318\" aria-describedby=\"caption-attachment-73056\" srcset=\"\/blog\/wp-ttn-blog\/uploads\/2025\/07\/Screenshot-from-2025-07-03-06-22-16.png 792w, \/blog\/wp-ttn-blog\/uploads\/2025\/07\/Screenshot-from-2025-07-03-06-22-16-300x120.png 300w, \/blog\/wp-ttn-blog\/uploads\/2025\/07\/Screenshot-from-2025-07-03-06-22-16-768x308.png 768w, \/blog\/wp-ttn-blog\/uploads\/2025\/07\/Screenshot-from-2025-07-03-06-22-16-624x251.png 624w\" sizes=\"(max-width: 792px) 100vw, 792px\" \/><\/p>\n<p id=\"caption-attachment-73056\" class=\"wp-caption-text\">Standalone launch sequence<\/p>\n<\/div>\n<h1><\/h1>\n<h2>EHR (Embedded) Launch Sequence<\/h2>\n<p>EHR (Embedded) Launch Sequence is same as standalone, <strong>except<\/strong>:<\/p>\n<ul>\n<li>App is launched from within the EHR.<\/li>\n<li>EHR passes a launch context and <strong>iss<\/strong> (FHIR base URL).<\/li>\n<\/ul>\n<div id=\"attachment_73058\" class=\"wp-caption alignnone\" style=\"width: 921px;\">\n<p><img decoding=\"async\" loading=\"lazy\" class=\"size-full wp-image-73058\" src=\"https:\/\/www.tothenew.com\/blog\/wp-ttn-blog\/uploads\/2025\/07\/Screenshot-from-2025-07-03-06-28-11.png\" alt=\"Embedded launch sequence\" width=\"911\" height=\"348\" aria-describedby=\"caption-attachment-73058\" srcset=\"\/blog\/wp-ttn-blog\/uploads\/2025\/07\/Screenshot-from-2025-07-03-06-28-11.png 911w, \/blog\/wp-ttn-blog\/uploads\/2025\/07\/Screenshot-from-2025-07-03-06-28-11-300x115.png 300w, \/blog\/wp-ttn-blog\/uploads\/2025\/07\/Screenshot-from-2025-07-03-06-28-11-768x293.png 768w, \/blog\/wp-ttn-blog\/uploads\/2025\/07\/Screenshot-from-2025-07-03-06-28-11-624x238.png 624w\" sizes=\"(max-width: 911px) 100vw, 911px\" \/><\/p>\n<p id=\"caption-attachment-73058\" class=\"wp-caption-text\">Embedded launch sequence<\/p>\n<\/div>\n<h2><\/h2>\n<h2>Step-by-Step Implementation<\/h2>\n<h3>Step 1: Register Your App<\/h3>\n<p>Register your app with the desired FHIR platform (Epic, Cerner etc) to get client credentials and configure redirect URI.<\/p>\n<h3>Step 2: Setup Express App (app.ts)<\/h3>\n<pre>import express, { Request, Response, NextFunction } from 'express';\r\nimport authRoutes from '.\/routes\/auth';\r\nimport { appConfig } from '.\/config';\r\n\r\nconst app = express();\r\napp.use(express.json());\r\n\r\napp.use('\/auth', authRoutes);\r\napp.listen(appConfig.port, () =&gt; {\r\n console.log(`Auth Server running at ${appConfig.origin}`);\r\n});<\/pre>\n<h3>Step 3: Define Routes (routes\/auth.ts)<\/h3>\n<pre>import express from 'express';\r\nimport {\r\n standaloneLaunch,\r\n standaloneLaunchCallback,\r\n embeddedLaunch,\r\n embeddedLaunchCallback,\r\n} from '..\/controllers\/auth';\r\n\r\nconst router = express.Router();\r\nrouter.get('\/standalone\/:provider', standaloneLaunch);\r\nrouter.get('\/callback', standaloneLaunchCallback);\r\nrouter.get('\/embedded', embeddedLaunch);\r\nrouter.get('\/embeddedCallback', embeddedLaunchCallback);\r\nexport default router;<\/pre>\n<h3>Step 4: Add Configuration File in root folder (config.ts)<\/h3>\n<pre>import { EhrProvider } from '.\/enums\/ehrProvider'\r\nimport { EhRAuthConfig, AppConfig } from '.\/types';\r\nrequire('dotenv').config();\r\n\r\nexport const appConfig: AppConfig = {\r\n \u00a0\u00a0port: process.env.PORT! || '3000',\r\n \u00a0\u00a0host: process.env.HOST!,\r\n \u00a0\u00a0origin: `${process.env.HOST}:${process.env.PORT}`,\r\n}\r\n\r\nexport const ehrAuthConfig: Record&lt;EhrProvider, EhRAuthConfig&gt; = {\r\n \u00a0\u00a0[EhrProvider.EPIC]: {\r\n \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0authorizationUrl: process.env.EPIC_AUTH_URL!,\r\n \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0tokenUrl: process.env.EPIC_TOKEN_URL!,\r\n \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0clientId: process.env.EPIC_CLIENT_ID!,\r\n \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0standaloneRedirectUrl: appConfig.origin + '\/auth\/callback',\r\n \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0embeddedRedirectUrl: appConfig.origin + '\/auth\/embeddedCallback',\r\n \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0fhirApiBase: process.env.EPIC_FHIR_API_BASE!,\r\n \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0scope: 'openid profile user\/Patient.read patient\/MedicationRequest.write'\r\n \u00a0\u00a0},\r\n \u00a0\u00a0[EhrProvider.CERNER]: {\r\n \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0authorizationUrl: process.env.CERNER_AUTH_URL!,\r\n \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0tokenUrl: process.env.CERNER_TOKEN_URL!,\r\n \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0clientId: process.env.CERNER_CLIENT_ID!,\r\n \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0standaloneRedirectUrl: appConfig.origin + '\/auth\/callback',\r\n \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0embeddedRedirectUrl: appConfig.origin + '\/auth\/embeddedCallback',\r\n \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0fhirApiBase: process.env.CERNER_FHIR_API_BASE!,\r\n \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0scope: 'openid profile user\/Patient.read'\r\n \u00a0\u00a0}\r\n}<\/pre>\n<h3>Step 5: Environment Variables (.env)<\/h3>\n<pre># App\r\n\r\nPORT=3000\r\nHOST=http:\/\/localhost\r\n\r\n# EPIC AUTH CONFIG\r\n\r\nEPIC_AUTH_URL=https:\/\/fhir.epic.com\/interconnect-fhir-oauth\/oauth2\/authorize\r\nEPIC_TOKEN_URL=https:\/\/fhir.epic.com\/interconnect-fhir-oauth\/oauth2\/token\r\nEPIC_CLIENT_ID=&lt;your-client-id&gt;\r\nEPIC_FHIR_API_BASE=https:\/\/fhir.epic.com\/interconnect-fhir-oauth\/api\/FHIR\/R4\r\n\r\n# CERNER AUTH CONFIG\r\n\r\nCERNER_AUTH_URL=https:\/\/authorization.cerner.com\/tenants\/ec2458f2-1e24-41c8-b71b-0e701af7583d\/protocols\/oauth2\/profiles\/smart-v1\/personas\/provider\/authorize\r\nCERNER_TOKEN_URL=https:\/\/authorization.cerner.com\/tenants\/ec2458f2-1e24-41c8-b71b-0e701af7583d\/protocols\/oauth2\/profiles\/smart-v1\/token\r\nCERNER_CLIENT_ID=&lt;your-client-id&gt;\r\nCERNER_FHIR_API_BASE=https:\/\/fhir-ehr-code.cerner.com\/r4\/ec2458f2-1e24-41c8-b71b-0e701af7583d<\/pre>\n<h3>Step 4: Add Controllers (controllers\/auth.ts)<\/h3>\n<pre>import { Request, Response } from 'express';\r\nimport { ehrAuthConfig } from '..\/config';\r\nimport { EhrProvider, HttpStatusCode, HttpMethod } from '..\/enums';\r\nimport { getWellKnownSmartConfiguration } from '..\/services\/fhir.service';\r\n\r\nexport const standaloneLaunch = (req: Request, res: Response): void =&gt; {\r\n \u00a0\u00a0const provider = req.params.provider as EhrProvider;\r\n \u00a0\u00a0if (!provider) {\r\n \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0console.log('Missing emr param')\r\n \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0res.status(HttpStatusCode.BAD_REQUEST).send('Missing emr param');\r\n \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0return\r\n \u00a0\u00a0}\r\n\r\n\u00a0\u00a0\u00a0const authConfig = ehrAuthConfig[provider]\r\n\r\n \u00a0\u00a0try {\r\n \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0const authParams = new URLSearchParams({\r\n \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0response_type: \"code\",\r\n \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0  \u00a0client_id: authConfig.clientId,\r\n \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0redirect_uri: authConfig.standaloneRedirectUrl,\r\n \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0scope: authConfig.scope,\r\n \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0aud: authConfig.fhirApiBase,\r\n \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0state: provider,\r\n \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0});\r\n \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0const redirectUrl = `${authConfig.authorizationUrl}?${authParams.toString()}`;\r\n \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0res.redirect(redirectUrl);\r\n \u00a0\u00a0} catch (error) {\r\n \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0console.error('Error:', (error as Error).message);\r\n \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0res.status(HttpStatusCode.INTERNAL_SERVER_ERROR).send('Internal server error');\r\n \u00a0\u00a0}\r\n};\r\n\r\nexport const standaloneLaunchCallback = async (req: Request, res: Response): Promise&lt;void&gt; =&gt; {\r\n \u00a0\u00a0const { code, state } = req.query;\r\n\u00a0\u00a0\u00a0const authConfig = ehrAuthConfig[state as EhrProvider]\r\n \u00a0\u00a0try {\r\n \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0const params = new URLSearchParams({\r\n \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0grant_type: 'authorization_code',\r\n \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0code,\r\n \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0redirect_uri: authConfig.standaloneRedirectUrl,\r\n \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0client_id: authConfig.clientId,\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0});\r\n\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\/\/ Exchanges  Auth Code for an Access Token\r\n \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0const response = await fetch(authConfig.tokenUrl, {\r\n \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0method: HttpMethod.POST,\r\n \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0headers: {\r\n \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0'Content-Type': 'application\/x-www-form-urlencoded',\r\n \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0},\r\n \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0body: params.toString(),\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0});\r\n\r\n \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0if (!response.ok) {\r\n \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0const errorData = await response.text();\r\n \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0console.error('Token exchange failed:', errorData);\r\n \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0res.status(HttpStatusCode.INTERNAL_SERVER_ERROR).send('Token exchange failed');\r\n \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0return;\r\n \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0}\r\n \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0const data = await response.json()\r\n \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0const { access_token, token_type, id_token, scope } = data;\r\n \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0res.json({ access_token, token_type, id_token, scope });\r\n \u00a0\u00a0} catch (error) {\r\n \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0res.status(HttpStatusCode.INTERNAL_SERVER_ERROR).send('Internal server error');\r\n \u00a0\u00a0}\r\n}\r\n\r\nfunction getEhrProviderByIssuer(fhirApiBase: string): EhrProvider {\r\n \u00a0\u00a0return Object.entries(ehrAuthConfig).find(\r\n \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0([, config]) =&gt; config.fhirApiBase === fhirApiBase\r\n \u00a0\u00a0)?.[0] as EhrProvider;\r\n}\r\n\r\nlet tokenUrl: string\r\n\r\nexport const embeddedLaunch = async (req: Request, res: Response): Promise&lt;any&gt; =&gt; {\r\n \u00a0\u00a0const fhirServerUrl: any = req.query.iss!;\r\n \u00a0\u00a0const launchContext: any = req.query.launch!;\r\n \u00a0\u00a0const ehrProvider = getEhrProviderByIssuer(fhirServerUrl)\r\n \u00a0\u00a0const authConfig = ehrAuthConfig[ehrProvider]\r\n\r\n \u00a0\u00a0if (!fhirServerUrl || !launchContext) {\r\n \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0console.log('Missing iss or launch parameter')\r\n \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0return res.status(HttpStatusCode.BAD_REQUEST).send('Missing iss or launch parameter.');\r\n\u00a0\u00a0\u00a0}\r\n\r\n \u00a0\u00a0try {\r\n \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0const smartConfig = await getWellKnownSmartConfiguration(fhirServerUrl)\r\n \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0const authorizeUrl = smartConfig.authorization_endpoint;\r\n \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0tokenUrl = smartConfig.token_endpoint;\r\n\r\n \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0const authParams = new URLSearchParams({\r\n \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0response_type: \"code\",\r\n \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0client_id: authConfig.clientId,\r\n \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0redirect_uri: authConfig.embeddedRedirectUrl,\r\n \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0scope: \"launch patient\/*.read\",\r\n \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0launch: launchContext.toString(),\r\n \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0aud: fhirServerUrl.toString(),\r\n \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0state: ehrProvider\r\n \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0}\r\n \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0const redirectUrl = `${authorizeUrl}?${authParams.toString()}`;\r\n \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0res.redirect(redirectUrl);\r\n \u00a0\u00a0} catch (error) {\r\n \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0res.status(HttpStatusCode.INTERNAL_SERVER_ERROR).send(\"Failed to launch\");\r\n \u00a0\u00a0}\r\n}\r\n\r\nexport const embeddedLaunchCallback = async (req: Request, res: Response): Promise&lt;any&gt; =&gt; {\r\n \u00a0\u00a0const { code, state } = req.query;\r\n \u00a0\u00a0const authConfig = ehrAuthConfig[state as EhrProvider]\r\n\r\n \u00a0\u00a0try {\r\n \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0const tokenParams = new URLSearchParams({\r\n \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0grant_type: 'authorization_code',\r\n \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0code: code as string,\r\n \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0client_id: authConfig.clientId,\r\n \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0redirect_uri: authConfig.embeddedRedirectUrl,\r\n \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0});\r\n \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0const tokenResponse = await fetch(tokenUrl, {\r\n \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0method: HttpMethod.POST,\r\n \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0headers: {\r\n \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0'Content-Type': 'application\/x-www-form-urlencoded',\r\n \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0}\r\n \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0body: tokenParams.toString(),\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0});\r\n\r\n \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0if (!tokenResponse.ok) {\r\n \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0const errorText = await tokenResponse.text();\r\n \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0return res.status(HttpStatusCode.INTERNAL_SERVER_ERROR).send('Error exchanging code for token');\r\n \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0}\r\n \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0const tokenData = await tokenResponse.json();\r\n \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0const accessToken = tokenData.access_token as string;\r\n \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0res.send(`Access token received! ${accessToken}`);\r\n \u00a0\u00a0} catch (err) {\r\n \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0res.status(HttpStatusCode.INTERNAL_SERVER_ERROR).send('Unexpected error during token exchange');\r\n \u00a0\u00a0}\r\n}\r\n\r\nmodule.exports = {\r\n \u00a0\u00a0standaloneLaunch,\r\n \u00a0\u00a0standaloneLaunchCallback,\r\n \u00a0\u00a0embeddedLaunch,\r\n \u00a0\u00a0embeddedLaunchCallback,\r\n};<\/pre>\n<p><em><strong>Note<\/strong>: In standalone launch, EHR provider\u2019s name (e.g Epic or Cerner) is passed in state parameter. This allows the callback handler to determine which config to use when exchanging auth code for a token.<\/em><\/p>\n<h2>Testing Your Application<\/h2>\n<h3>Standalone Launch<\/h3>\n<p>Start your server: <strong>npm run start<\/strong> and try:<br \/>\n<em>Epic: <a href=\"https:\/\/localhost:3000\/auth\/standalone\/epic\" rel=\"nofollow\">http:\/\/localhost:3000\/auth\/standalone\/epic<\/a><\/em><br \/>\n<em>Cerner: <a href=\"https:\/\/localhost:3000\/auth\/standalone\/epic\" rel=\"nofollow\">http:\/\/localhost:3000\/auth\/standalone\/cerner<\/a><\/em><\/p>\n<p>You will be redirected to the EHR login screen. After successfully logging in, you will receive access token.<\/p>\n<h3>Embedded Launch<\/h3>\n<p>To simulate embedded launch use the SMART App Launcher with your embedded endpoint: http:\/\/localhost:3000\/auth\/embedded<\/p>\n<h2>Using token to access FHIR resources<\/h2>\n<p>Once you receive valid access token, your can now access protected patient data from the EHR database using FHIR Rest APIs. You need to pass access token in authorization header as a Bearer token.<\/p>\n<pre>GET &lt;fhir-server-url&gt;\/Patient\/{id}\r\nAuthorization: Bearer Nxfve4q3H9TKs5F5vf6kRYAZqz...<\/pre>\n<h1><\/h1>\n<h2>Adding Support for a New EHR<\/h2>\n<p>Adding support for another SMART on FHIR compliant EHR is <strong>simple, <\/strong>Just add another provider entry in <strong>ehrAuthConfig<\/strong> inside config.ts. <strong><em>No additional logic required!<\/em><\/strong><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Introduction In the rapidly evolving landscape of health-tech, interoperability and security are critical. SMART on FHIR (Substitutable Medical Applications, Reusable Technologies on Fast Healthcare Interoperability Resources) has emerged as a robust standard to create apps that integrate seamlessly with Electronic Health Record systems. This blog, which is meant for developers building healthcare apps, covered the [&hellip;]<\/p>\n","protected":false},"author":2108,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"iawp_total_views":263},"categories":[5876],"tags":[7423,6883,6884,7422,7405,6165,3043,7655,7573],"aioseo_notices":[],"_links":{"self":[{"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/posts\/73460"}],"collection":[{"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/users\/2108"}],"replies":[{"embeddable":true,"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/comments?post=73460"}],"version-history":[{"count":6,"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/posts\/73460\/revisions"}],"predecessor-version":[{"id":73741,"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/posts\/73460\/revisions\/73741"}],"wp:attachment":[{"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/media?parent=73460"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/categories?post=73460"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/tags?post=73460"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}