Building a SMART on FHIR-inspired Auth Server with NestJS
Technology Blogs

Building a SMART on FHIR-inspired Auth Server with NestJS

Maithili S
Software Engineer
Table of Content

In the digital health ecosystem, interoperability and security are paramount. The SMART on FHIR standard provides a powerful framework for securely connecting third-party applications to healthcare data sources using open standards like OAuth 2.0 and OpenID Connect.

This post dissects a NestJS-based proof-of-concept that implements a SMART on FHIR-inspired authorization server. We will explore how it handles multiple authentication workflows, secure token management, and fine-grained access control, using code examples directly from the repository.

Service Discovery: The .well-known/smart-configuration Endpoint

A core feature of a SMART on FHIR server is service discovery. A client application needs to know the server’s capabilities, such as its authorization and token endpoints, supported scopes, and signature algorithms. This is achieved via a public metadata endpoint.

The repository implements this in

src/well-known/well-known.controller.ts:

import { Controller, Get } from '@`nestjs/common`';

import { ConfigService } from '@`nestjs/config`';

('.well-known')

export class WellKnownController {

 constructor(private configService: ConfigService) {}

('smart-configuration')

 getSmartConfiguration() {

   const baseUrl = this.configService.get<string>('baseUrl');

   return {

     authorization_endpoint: `${baseUrl}/auth/authorize`,

     token_endpoint: `${baseUrl}/auth/token`,

     introspection_endpoint: `${baseUrl}/auth/introspect`,

     scopes_supported: [

       'openid',

       'fhirUser',

       'launch',

       '`launch/patient`',

       'patient/*.read',

       'user/*.read',

     ],

     response_types_supported: ['code', 'token'],

     grant_types_supported: [

       'authorization_code',

       'client_credentials',

     ],

     token_endpoint_auth_methods_supported: ['client_secret_basic'],

     // ... and other metadata

   };

 }

}

This endpoint immediately tells client applications how to interact with our server, which is the first step in any SMART on FHIR interaction.

Core Authentication Workflows

The server is designed to handle several distinct authentication scenarios.

1. Standalone Launch (OAuth 2.0 Authorization Code Flow)

This is the classic 3-legged OAuth flow where a user, outside of an EHR, authorizes an application to access their data.

Step 1: Authorization Request The application redirects the user to the /auth/authorize endpoint. The auth.controller.ts defines the structure for this request using a DTO.

From src/auth/dto/authorize.dto.ts:

import { IsString, IsNotEmpty, IsOptional } from 'class-validator';

export class AuthorizeDto {

 ()

()

 response_type: string; // e.g., 'code'

()

()

 client_id: string;

()

()

 redirect_uri: string;

()

()

 scope: string;

 // ... other parameters like 'state' and 'aud'

}

The controller validates this request, and if the client is valid, it would typically present a login and consent screen to the user. Upon approval, it generates a short-lived authorization code and redirects back to the redirect_uri provided by the client.

Step 2: Secure Token Exchange The client application then exchanges this code for an access token by making a backend POST request to the /auth/token endpoint with grant_type: ‘authorization_code’. The AuthService validates the code and, if successful, issues a JWT.

2. Backend Services (JWT-Based Authentication)

For service-to-service communication without a user, the server supports the OAuth 2.0 Client Credentials flow. We covered this in the previous post, but it’s a key part of the SMART on FHIR specification for trusted backend systems.

The ClientCredentialsStrategy validates the client_id and client_secret, and the /auth/token endpoint issues an access token when the grant_type is client_credentials.

3. Embedded / Contextual Launch

In a true SMART on FHIR scenario, an app is often launched from within an Electronic Health Record (EHR) portal. The EHR has already authenticated the user.

The specification handles this by passing a launch context and iss (issuer) parameter to the application. The application would then include these in its request to the /auth/authorize endpoint. The auth server would validate the iss parameter and use the launch token to fetch context (like the patient ID) without requiring the user to log in again.

While the repository’s DTOs and AuthService provide the necessary foundation, the specific logic to parse a launch parameter and bypass user login is a clear next step for a full EHR-embedded launch implementation.

Building Healthcare Apps With SMART On FHIR? Get Expert Guidance On Secure Oauth Implementation.

Token Management

The lifecycle of a token is critical for security.

JWT Creation, Signing, and Expiration The TokenService is responsible for minting tokens. It uses the standard @nestjs/jwt library to create signed JWTs with a configured expiration time.

From src/auth/token/token.service.ts:

import { Injectable } from '@`nestjs/common`';

import { JwtService } from '@`nestjs/jwt`';

import { ConfigService } from '@`nestjs/config`';

()

export class TokenService {

 constructor(

   private readonly jwtService: JwtService,

   private readonly configService: ConfigService,

 ) {}

 async generateAccessToken(client: any, scope: string): Promise<any> {

   const payload = {

     sub: client.clientId, // Subject

     aud: this.configService.get<string>('`fhir.audience`'), // Audience

     scope: scope, // Scopes

     // ... other claims

   };

   const token = this.jwtService.sign(payload);

   // ...

   return {

     access_token: token,

     token_type: 'Bearer',

     expires_in: this.configService.get<number>('`jwt.expiresIn`'),

     scope: scope,

   };

 }

}

Token Refresh Flow To improve user experience, long-lived refresh_tokens can be issued, allowing an application to obtain a new access_token without prompting the user again. This would involve extending the /auth/token endpoint to handle a grant_type: ‘refresh_token’. While the current implementation focuses on the primary flows, adding refresh token logic would be a standard enhancement.

Token Introspection and Verification How does a resource server (like our FHIR API) know a token is valid?

  1. Access Token Verification (Local): The resource server can validate the JWT’s signature, expiration, issuer (iss), and audience (aud) locally. This is handled by the JwtStrategy, which is used by the JwtAuthGuard.
  2. Token Introspection (Remote): For opaque tokens or as an extra security check, the server provides an /auth/introspect endpoint, as advertised in its smart-configuration. A resource server can post a token to this endpoint, and the auth server will respond with whether the token is active and what its metadata (like scopes) is.

From src/auth/auth.controller.ts:

('introspect')

(AuthGuard('client-credentials')) // The introspector must authenticate

 async introspect(@Body() body: IntrospectDto) {

   return this.authService.introspect(body.token);

 }
coma

Conclusion

This NestJS repository provides an excellent, practical foundation for a SMART on FHIR-inspired authorization server. It correctly implements service discovery, separates concerns for different authentication flows, and provides the necessary endpoints for a complete token lifecycle. By building on this base, developers can extend it to support more advanced features like contextual launches and refresh tokens, creating a truly robust and interoperable security solution for healthcare applications.

Maithili S

Maithili S

Software Engineer

Maithili is a front-end developer with 1+ years of experience, has proficiency in technologies like React.js, Redux, JavaScript, and UI Frameworks, and is experienced in creating responsive, ​​testable, and adaptive web applications. She loves to explore and learn new technologies as well.

Share This Blog

Read More Similar Blogs

Let’s Transform
Healthcare,
Together.

Partner with us to design, build, and scale digital solutions that drive better outcomes.

Location

5900 Balcones Dr, Ste 100-7286, Austin, TX 78731, United States

Contact form