Building a SMART on FHIR App in .NET: A Step-by-Step Guide

Introduction

Welcome to this comprehensive guide on building a SMART on FHIR application using .NET. SMART on FHIR is an open standards framework that enables apps to seamlessly and securely connect with electronic health records (EHRs) and other healthcare data sources. In this article, we will walk through the steps to create a SMART on FHIR app, from setting up the environment to implementing authentication, interacting with FHIR resources, and deploying the app.

Deep Dive into SMART on FHIR Architecture

Before diving into implementation, let's understand the architecture and components that make SMART on FHIR work.

The OAuth2 Authorization Flow in Detail

SMART on FHIR uses OAuth2 with specific extensions for healthcare. Here's a detailed breakdown of the authorization flow:

SMART on FHIR — OAuth2 Launch Flow saravanansubramanian.com nine numbered exchanges from EHR launch to authenticated FHIR API calls App SMART client EHR launch origin Authorization Server User clinician / patient Token Endpoint /token FHIR Server resource API Launch Request initiates from EHR context 1 Launch Response launch token · iss URL 2 Discovery GET /.well-known/smart-configuration 3 Authorization Request client_id · scope · state · redirect_uri 4 5 User authenticates and consents to requested scopes Authorization Code redirect back to App with code + state 6 Token Request POST with code + client credentials 7 Access Token + refresh token · patient context · scopes 8 FHIR API Requests Authorization: Bearer <access_token> 9

SMART App Launch Framework Components

1. Authorization Server

  • Handles user authentication and consent
  • Issues authorization codes and access tokens
  • Validates client credentials and redirect URIs
  • May be integrated with the EHR or standalone (e.g., Azure AD, Keycloak)

2. Resource Server (FHIR Server)

  • Hosts the FHIR API endpoints
  • Validates access tokens for each request
  • Enforces scope-based access control
  • Returns FHIR resources based on authorized permissions

3. Discovery Document (.well-known/smart-configuration)

  • Published at [fhir-base]/.well-known/smart-configuration
  • Contains URLs for authorization and token endpoints
  • Lists supported scopes, capabilities, and features
  • Enables dynamic client configuration without hardcoding URLs
// Example .well-known/smart-configuration response
{
  "authorization_endpoint": "https://ehr.example.com/auth/authorize",
  "token_endpoint": "https://ehr.example.com/auth/token",
  "registration_endpoint": "https://ehr.example.com/auth/register",
  "scopes_supported": ["launch", "patient/*.read", "user/*.read", "openid"],
  "response_types_supported": ["code"],
  "capabilities": ["launch-ehr", "launch-standalone", "client-public", "sso-openid-connect"]
}

Clinical Context

SMART on FHIR passes clinical context through the token response, enabling apps to know which patient, encounter, or user they're working with:

  • patient - The FHIR ID of the patient in context (e.g., "Patient/123")
  • encounter - The current clinical encounter, if applicable
  • fhirUser - The FHIR resource representing the current user (Practitioner, Patient, RelatedPerson)
  • need_patient_banner - Whether the app should display a patient context banner
  • smart_style_url - URL to CSS for matching EHR styling
// Example token response with clinical context
{
  "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "launch patient/*.read openid fhirUser",
  "patient": "123",
  "encounter": "456",
  "fhirUser": "Practitioner/789",
  "id_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
}

App Registration and Client Types

SMART on FHIR supports different client types with different security characteristics:

Confidential Clients (server-side applications):

  • Can securely store a client secret
  • Authenticate with client_id and client_secret
  • Preferred for server-to-server communication
  • Can use refresh tokens

Public Clients (browser-based or mobile apps):

  • Cannot securely store secrets
  • Must use PKCE (Proof Key for Code Exchange) for security
  • Authenticate with client_id only (no secret)
  • More restricted token lifetimes

Registration Requirements:

  • Client ID - Unique identifier for your application
  • Redirect URIs - Whitelisted callback URLs
  • Scopes - Requested permissions
  • App Name and Description - For user consent screens
  • Logo URL - Displayed during authorization
  • Terms of Service/Privacy Policy URLs - Required by some EHRs

Refresh Tokens and Session Management

Access tokens have limited lifespans. Refresh tokens enable long-running sessions without requiring re-authentication:

// Using a refresh token to get a new access token
public async Task<string> RefreshAccessTokenAsync(string refreshToken)
{
    using var httpClient = new HttpClient();

    var content = new FormUrlEncodedContent(new Dictionary<string, string>
    {
        ["grant_type"] = "refresh_token",
        ["refresh_token"] = refreshToken,
        ["client_id"] = _clientId,
        ["client_secret"] = _clientSecret // For confidential clients
    });

    var response = await httpClient.PostAsync(_tokenEndpoint, content);
    var responseContent = await response.Content.ReadAsStringAsync();

    var tokenResponse = JsonConvert.DeserializeObject<Dictionary<string, object>>(responseContent);
    return tokenResponse["access_token"].ToString();
}

Session Management Best Practices:

  • Store refresh tokens securely (encrypted, not in browser storage)
  • Implement token refresh before access token expires
  • Handle refresh token expiration gracefully (re-authenticate user)
  • Revoke tokens when user logs out
  • Monitor for token theft indicators (unusual IP, location changes)

Backend Services Authorization

For system-to-system communication without user interaction, SMART defines a Backend Services flow using asymmetric keys:

  • App generates a public/private key pair
  • Public key is registered with the authorization server
  • App creates a signed JWT assertion using the private key
  • JWT is exchanged for an access token (no user consent required)
  • Commonly used for batch processing, analytics, and data synchronization

Prerequisites

Before getting started, ensure you have the following tools and environment set up:

  • .NET SDK installed and configured.
  • Azure CLI installed and configured (if using Azure FHIR Server).
  • Access to a FHIR server with SMART on FHIR capabilities (e.g., FHIR Server for Azure).
  • Basic understanding of FHIR resources and RESTful APIs.
  • You can find all the code demonstrated in this tutorial on GitHub here

“Science is what you know. Philosophy is what you don’t know.” ~ Bertrand Russell

Step 1 of 5: Setting Up the Project

The first step in building a SMART on FHIR app is to set up your .NET project. We'll use the .NET CLI to create a new web application that will serve as the foundation for our SMART on FHIR app.

Creating the .NET Project

Create a new .NET Core web application using the following command:

dotnet new webapp -n SmartOnFhirApp

This command initializes a new .NET Core web application. Navigate to the project directory and open the `SmartOnFhirApp.csproj` file to add the required dependencies:

<ItemGroup>
    <!-- FHIR Client -->
    <PackageReference Include="Hl7.Fhir.R4" Version="4.3.0" />

    <!-- OAuth2 Library -->
    <PackageReference Include="Microsoft.Identity.Client" Version="4.30.1" />

    <!-- JSON Processing -->
    <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
</ItemGroup>

These dependencies include the FHIR client for interacting with FHIR resources and the Microsoft Identity Client (MSAL) library for handling SMART on FHIR authentication flows.

Step 2 of 5: Implementing SMART on FHIR Authentication

SMART on FHIR apps must authenticate and authorize users before accessing FHIR resources. This is typically done using OAuth2, which provides secure access tokens that the app uses to interact with the FHIR server.

Obtaining an Access Token

To obtain an access token, you'll need to perform the OAuth2 Authorization Code Flow. Here's a simplified example of how to handle this in .NET:

using System;
using System.Threading.Tasks;
using Microsoft.Identity.Client;

public class OAuth2Client
{
    public static async Task Main(string[] args)
    {
        var clientId = "your-client-id";
        var clientSecret = "your-client-secret";
        var tenantId = "your-tenant-id";
        var authority = $"https://login.microsoftonline.com/{tenantId}";

        var app = ConfidentialClientApplicationBuilder.Create(clientId)
            .WithClientSecret(clientSecret)
            .WithAuthority(new Uri(authority))
            .Build();

        var scopes = new[] { "https://your-fhir-server-url/.default" };

        try
        {
            var result = await app.AcquireTokenForClient(scopes).ExecuteAsync();
            Console.WriteLine($"Access Token: {result.AccessToken}");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Error acquiring token: {ex.Message}");
        }
    }
}

This example demonstrates how to obtain an OAuth2 access token using the Microsoft Identity Client (MSAL) library. The access token is then used to authenticate requests to the FHIR server.

Step 3 of 5: Interacting with FHIR Resources

Once authenticated, your SMART on FHIR app can interact with FHIR resources. Below is an example of how to use the FHIR client to search for Patient resources:

using Hl7.Fhir.Model;
using Hl7.Fhir.Rest;
using System;

public class FHIRClient
{
    public static async Task Main(string[] args)
    {
        // Initialize FHIR client
        var client = new FhirClient("http://hapi.fhir.org/baseR4");

        // Perform a search for patients by name using async
        var bundle = await client.SearchAsync<Patient>(new string[] { "name=Smith" });

        Console.WriteLine($"Found {bundle.Total} patients named Smith");
    }
}

This code snippet shows how to perform a search operation for patients with the last name "Smith." The FHIR client communicates with the server using the access token obtained in the previous step.

Step 4 of 5: Building the User Interface

A SMART on FHIR app typically includes a user interface that allows users to interact with healthcare data. In .NET, you can use frameworks like ASP.NET Core with Razor Pages or Blazor to build your UI. For this example, we'll assume you're using Razor Pages.

Razor Pages UI Example

Here is an example of a simple Razor Page that displays a welcome message:

@page
@model IndexModel
@{
    ViewData["Title"] = "Home Page";
}

<div class="text-center">
    <h1 class="display-4">Welcome to the SMART on FHIR App</h1>
    <p>This is your starting point for building a FHIR-based healthcare application.</p>
</div>

This is a basic Razor Page that serves as the foundation for your SMART on FHIR app's UI. You can extend this example to include forms, tables, and other components that interact with FHIR data.

Step 5 of 5: Deploying the SMART on FHIR App

Deploying your SMART on FHIR app involves making it accessible to users and ensuring it integrates correctly with healthcare systems. If you're deploying on-premises or to a cloud provider like Azure, consider using containerization (e.g., Docker) to streamline the deployment process.

Dockerizing the Application

Here’s a basic Dockerfile to containerize your .NET application:

# Use an official .NET runtime as a parent image
FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base
WORKDIR /app

# Use an official .NET SDK as a build environment
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
WORKDIR /src
COPY ["SmartOnFhirApp/SmartOnFhirApp.csproj", "SmartOnFhirApp/"]
RUN dotnet restore "SmartOnFhirApp/SmartOnFhirApp.csproj"
COPY . .
WORKDIR "/src/SmartOnFhirApp"
RUN dotnet build "SmartOnFhirApp.csproj" -c Release -o /app/build

FROM build AS publish
RUN dotnet publish "SmartOnFhirApp.csproj" -c Release -o /app/publish

# Build the runtime image
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "SmartOnFhirApp.dll"]

This Dockerfile sets up the environment to run your SMART on FHIR app in a container. Once containerized, you can deploy the application to your preferred environment.

Conclusion

In this article, we've covered the process of building a SMART on FHIR app using .NET, from setting up the project to implementing authentication, interacting with FHIR resources, and deploying the application. SMART on FHIR is a powerful framework that enables seamless and secure access to healthcare data, making it an essential tool for modern healthcare applications.

In the next article in this series, we will explore FHIR profiles and demonstrate their practical application using Canadian Core profiles as an example. You'll learn how profiles customize the base FHIR specification to meet specific jurisdictional requirements.