DL
Back to Blog
TechFebruary 2, 2026·5 min read

SMART on FHIR Authentication with Keycloak

How FhirHub implements SMART on FHIR authentication using Keycloak with PKCE-secured Authorization Code flow and a fine-grained RBAC matrix controlling access to clinical resources.

D

David Le

SMART on FHIR Authentication with Keycloak

By David Le -- Part 4 of the FhirHub Series

Authentication in healthcare applications isn't optional -- it's regulated. SMART on FHIR is the industry-standard framework for securing FHIR-based applications. FhirHub implements SMART on FHIR using Keycloak as the identity provider, with PKCE for public client security and a fine-grained RBAC matrix.

Why SMART on FHIR?

SMART (Substitutable Medical Applications, Reusable Technologies) defines how applications launch within EHR systems and obtain authorized access to FHIR data. It builds on OAuth 2.0 and OpenID Connect, adding healthcare-specific scopes like patient/*.read and launch/patient.

FhirHub doesn't run inside an EHR, but it adopts the SMART on FHIR security patterns because they're well-designed and widely understood in the healthcare developer community.

The Authentication Flow

FhirHub uses the Authorization Code flow with PKCE (Proof Key for Code Exchange), which is the recommended flow for browser-based applications:

1. User visits FhirHub → redirected to Keycloak login
2. User enters credentials
3. Keycloak returns authorization code
4. Frontend exchanges code for JWT (using PKCE code_verifier)
5. JWT included in all API requests
6. API validates JWT signature via Keycloak OIDC discovery

Frontend: Keycloak-JS Integration

The frontend initializes Keycloak with a singleton pattern:

// frontend/src/lib/keycloak.ts
import Keycloak from "keycloak-js";

let instance: Keycloak | null = null;
let _initialized = false;

export function getKeycloak(): Keycloak {
  if (!instance) {
    instance = new Keycloak({
      url: process.env.NEXT_PUBLIC_KEYCLOAK_URL || "http://localhost:8180",
      realm: process.env.NEXT_PUBLIC_KEYCLOAK_REALM || "fhirhub",
      clientId:
        process.env.NEXT_PUBLIC_KEYCLOAK_CLIENT_ID || "fhirhub-frontend",
    });
  }
  return instance;
}

export async function initKeycloak(
  options: {
    onLoad?: "login-required" | "check-sso";
    silentCheckSsoRedirectUri?: string;
  } = {}
): Promise<boolean> {
  const kc = getKeycloak();
  if (_initialized) {
    return kc.authenticated ?? false;
  }
  _initialized = true;
  return kc.init({ ...options, pkceMethod: "S256" });
}

Key design decisions:

  • Singleton pattern prevents multiple Keycloak instances from being created during React re-renders
  • PKCE with S256 eliminates the need for a client secret in the browser
  • login-required onLoad means unauthenticated users are immediately redirected to Keycloak
  • Environment variables allow configuration without code changes

Backend: JWT Validation

The .NET API validates every request's JWT token using the JWT Bearer middleware:

// Program.cs
var keycloakSettings = builder.Configuration.GetSection("Keycloak");

builder.Services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
    options.Authority = keycloakSettings["Authority"];
    options.Audience = keycloakSettings["Audience"];
    options.RequireHttpsMetadata =
        keycloakSettings.GetValue<bool>("RequireHttpsMetadata", true);

    options.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateIssuer = true,
        ValidateAudience = true,
        ValidateLifetime = true,
        NameClaimType = "preferred_username",
        RoleClaimType = ClaimTypes.Role
    };

    options.Events = new JwtBearerEvents
    {
        OnAuthenticationFailed = context =>
        {
            Log.Warning("Authentication failed: {Error}",
                context.Exception.Message);
            return Task.CompletedTask;
        }
    };
});

The middleware automatically:

  1. Fetches Keycloak's OIDC discovery document to find the JWKS endpoint
  2. Downloads the signing keys
  3. Validates the JWT signature, issuer, audience, and expiration
  4. Maps claims to the HttpContext.User principal

Role-Based Access Control

The Role Hierarchy

FhirHub defines six roles with progressively restricted access:

RoleDescription
adminFull system access including user management and audit logs
practitionerClinical read/write access, export management
nurseClinical read access, can write vitals, manage alerts
front_deskPatient demographics read-only
patientOwn data read-only

The Policy Matrix

Authorization policies are defined in a declarative matrix:

// Authorization/AuthorizationPolicies.cs
public static readonly Dictionary<string, string[]> PolicyRoles = new()
{
    [CanReadPatients]        = ["admin", "practitioner", "nurse", "front_desk", "patient"],
    [CanWritePatients]       = ["admin", "practitioner"],
    [CanReadVitals]          = ["admin", "practitioner", "nurse", "patient"],
    [CanWriteVitals]         = ["admin", "practitioner", "nurse"],
    [CanReadConditions]      = ["admin", "practitioner", "nurse", "patient"],
    [CanWriteConditions]     = ["admin", "practitioner"],
    [CanReadMedications]     = ["admin", "practitioner", "nurse", "patient"],
    [CanWriteMedications]    = ["admin", "practitioner"],
    [CanReadLabs]            = ["admin", "practitioner", "nurse", "patient"],
    [CanOrderLabs]           = ["admin", "practitioner"],
    [CanReadClinicalOverviews] = ["admin", "practitioner", "nurse"],
    [CanViewDashboard]       = ["admin", "practitioner", "nurse"],
    [CanManageAlerts]        = ["admin", "practitioner", "nurse"],
    [CanManageExports]       = ["admin", "practitioner"],
    [CanDeleteExports]       = ["admin"],
    [CanManageUsers]         = ["admin"],
    [CanViewAuditLogs]       = ["admin"],
};

That's 17 policies across 6 roles. Each policy lists which roles are authorized. This matrix is registered at startup:

builder.Services.AddAuthorization(options =>
{
    foreach (var (policyName, roles) in AuthorizationPolicies.PolicyRoles)
    {
        options.AddPolicy(policyName, policy => policy.RequireRole(roles));
    }

    options.AddPolicy(AuthorizationPolicies.PatientDataAccess, policy =>
        policy.Requirements.Add(new PatientDataRequirement()));
});

Resource-Level Authorization

Beyond role checks, some endpoints also enforce resource-level access through the PatientDataAccess policy. This ensures patients can only access their own data:

// PatientsController.cs
[HttpGet("{id}")]
[Authorize(Policy = AuthorizationPolicies.CanReadPatients)]
[Authorize(Policy = AuthorizationPolicies.PatientDataAccess)]
public async Task<IActionResult> GetById(string id, CancellationToken ct)

Notice the stacked [Authorize] attributes -- both policies must pass. The role check (CanReadPatients) and the resource check (PatientDataAccess) are evaluated independently.

Keycloak Claims Transformation

Keycloak puts roles inside realm_access.roles in the JWT. .NET expects them as standard role claims. A custom IClaimsTransformation bridges the gap:

builder.Services.AddTransient<
    IClaimsTransformation,
    KeycloakClaimsTransformer>();

This transformer extracts roles from the Keycloak-specific JWT structure and adds them as standard .NET role claims that RequireRole() can evaluate.

SMART on FHIR Tools

FhirHub includes a SMART on FHIR developer tools page with three components:

  • Launch Simulator (launch-simulator.tsx) -- Simulates a SMART launch with configurable context parameters
  • Token Inspector (token-inspector.tsx) -- Decodes and displays the current JWT token claims
  • Scope Visualizer (scope-visualizer.tsx) -- Shows which FHIR scopes are granted to the current session

These tools are invaluable for debugging authentication issues during development.

What's Next

In Part 5, we'll explore the FHIR R4 resources that FhirHub implements and how they map to the API's DTOs.


Find the source code on GitHub Connect on LinkedIn

Related Projects

Featured

FhirHub

A healthcare data management platform built on the HL7 FHIR R4 standard, providing a comprehensive web interface for managing patient clinical data including vitals, conditions, medications, lab orders, and bulk data exports with role-based access control and full audit logging.

Next.js 16
React 19
Typescript
Tailwind CSS 4
+8