OAuth 2.0 for FHIR
OAuth 2.0 is the authorization framework used by SMART on FHIR, enabling applications to obtain limited access to FHIR resources on behalf of users.
OAuth 2.0 Grant Types
OAuth 2.0 grant types for FHIR
| Grant Type | Flow | Use Case | Security |
|---|---|---|---|
| Authorization Code | User redirected to auth server, returns with code, exchange for tokens | Web apps, EHR launch (SMART) | High (code exchanged server-side) |
| Authorization Code + PKCE | Same as auth code + code verifier/challenge | Mobile apps, SPA (SMART standalone) | High (prevents code interception) |
| Client Credentials | Client authenticates directly with client_id/secret | System-to-system (B2B) | Medium (requires secret protection) |
| Refresh Token | Exchange refresh token for new access token | Token renewal without user interaction | High (long-lived, must be protected) |
Authorization Code Flow with PKCE
OAuth 2.0 Auth Code Flow with PKCE
Loading diagram...
SMART on FHIR requires PKCE (Proof Key for Code Exchange) for public clients:
- App generates code_verifier (random string)
- App creates code_challenge = SHA256(code_verifier)
- App redirects to authorization endpoint with code_challenge
- User authenticates and authorizes
- Auth server returns authorization code
- App exchanges code for tokens with code_verifier
- Auth server verifies code_challenge matches
- Tokens returned (access, ID, refresh)
PKCE is Mandatory
SMART on FHIR v2 requires PKCE for all public clients (mobile apps, SPAs). Never use Implicit Flow for new implementations.
SMART Authorization Flow
SMART on FHIR authorization flow
Loading diagram...
OpenID Connect (OIDC)
OpenID Connect (OIDC) adds an identity layer on top of OAuth 2.0, providing standardized user authentication and identity information via ID tokens.
OIDC Token Types
OIDC token types
| Token | Purpose | Format | Lifetime |
|---|---|---|---|
| ID Token | User identity information | JWT | Short (1 hour) |
| Access Token | Access FHIR resources | JWT or opaque | Short (1 hour) |
| Refresh Token | Obtain new access tokens | Opaque | Long (30 days) |
ID Token Claims
- iss: Issuer identifier (Cognito user pool URL)
- sub: Subject identifier (user ID)
- aud: Audience (client app ID)
- exp: Expiration time
- iat: Issued at time
- name: User's full name
- email: User's email address
- fhirUser: Reference to FHIR Practitioner/Patient (SMART-specific)
UserInfo Endpoint
The UserInfo endpoint provides additional user claims:
GET /oauth2/userInfo HTTP/1.1
Host: example.auth.ap-southeast-2.amazoncognito.com
Authorization: Bearer <access_token>
Response:
{
"sub": "user-123",
"name": "Dr. Jane Smith",
"email": "jane.smith@example.com",
"fhirUser": "Practitioner/practitioner-456"
}SMART Scopes
SMART scopes provide granular access control to FHIR resources, specifying which resources can be accessed and what operations are permitted.
Scope Categories
SMART scope types
| Scope | Description | Example |
|---|---|---|
| launch | App is launched in a specific context | launch, launch/patient, launch/encounter |
| openid | OIDC authentication, returns ID token | openid |
| fhirUser | Returns fhirUser claim in ID token | fhirUser |
| launch/patient | Launched in context of specific patient | launch/patient |
| patient/[resource].[access] | Access to patient-context resources | patient/Patient.read, patient/Observation.read |
| user/[resource].[access] | Access to user-context resources | user/Patient.read, user/Practitioner.read |
| system/[resource].[access] | System-level access (no patient context) | system/Metadata.read |
| offline_access | Request refresh token for offline access | offline_access |
Scope Syntax
SMART scopes follow the pattern: [context]/[resource].[access]
- Context: patient, user, or system
- Resource: FHIR resource type (Patient, Observation, etc.) or * for all
- Access: read, write, or * for both
# Read-only access to Patient in patient context
patient/Patient.read
# Read/write access to Observations in user context
user/Observation.read user/Observation.write
# All resources, read-only, patient context
patient/*.read
# Launch without specific patient
launch
# Launch with specific patient
launch/patient
# Request refresh token
offline_accessScope Request
Scopes are requested during authorization:
GET /oauth2/authorize?
response_type=code&
client_id=smart-app-client-id&
redirect_uri=https://smart-app.example.com/callback&
scope=launch/patient patient/Patient.read patient/Observation.read openid fhirUser&
state=abc123&
code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&
code_challenge_method=S256Least Privilege
Request only the scopes your application needs. Over-scoping may cause authorization failures and violates least privilege principles.
Amazon Cognito for SMART
Amazon Cognito provides a managed OAuth 2.0/OIDC identity provider that can be configured for SMART on FHIR authentication.
Cognito Components
- User Pools: User directory with authentication
- Identity Pools: AWS credentials for authenticated users
- Hosted UI: Pre-built login pages
- App Clients: Application registration
- Domains: Custom authentication domain
Cognito Configuration for SMART
- Enable OAuth 2.0 flows (authorization code)
- Configure allowed OAuth scopes (openid, fhir/user, etc.)
- Set callback URLs for SMART app
- Configure token validity (1 hour access, 30 days refresh)
- Enable PKCE support
- Add custom attributes (fhirUser, practitioner_role)
- Configure MFA for enhanced security
Terraform: Cognito User Pool
# Cognito User Pool for SMART on FHIR
resource "aws_cognito_user_pool" "smart_pool" {
name = "smart-fhir-user-pool"
# Password policy
password_policy {
minimum_length = 12
require_lowercase = true
require_numbers = true
require_symbols = true
require_uppercase = true
temporary_password_validity_days = 7
}
# Schema attributes
schema {
name = "email"
attribute_data_type = "String"
required = true
mutable = true
}
schema {
name = "name"
attribute_data_type = "String"
required = true
mutable = true
}
schema {
name = "fhir_user"
attribute_data_type = "String"
required = false
mutable = true
}
schema {
name = "practitioner_role"
attribute_data_type = "String"
required = false
mutable = true
}
# MFA configuration
mfa_configuration = "OPTIONAL"
software_token_mfa_configuration {
enabled = true
}
# User pool tags
tags = {
Name = "smart-fhir-pool"
Purpose = "SMART on FHIR Authentication"
Compliance = "IRAP-PROTECTED"
}
}
# Cognito User Pool Client (SMART App)
resource "aws_cognito_user_pool_client" "smart_app" {
name = "smart-fhir-app"
user_pool_id = aws_cognito_user_pool.smart_pool.id
# OAuth flows
allowed_oauth_flows = ["code", "implicit"]
allowed_oauth_flows_user_pool_client = true
# OAuth scopes
allowed_oauth_scopes = [
"openid",
"email",
"profile",
"fhir/user",
"fhir/patient",
"launch",
"offline_access"
]
# Callback URLs
callback_urls = [
"https://smart-app.example.com/callback",
"https://smart-app.example.com/launch"
]
# Logout URLs
logout_urls = [
"https://smart-app.example.com/logout"
]
# Supported identity providers
supported_identity_providers = ["COGNITO"]
# Token validity
access_token_validity = 1 # 1 hour
id_token_validity = 1 # 1 hour
refresh_token_validity = 30 # 30 days
# Prevent token revocation
prevent_user_existence_errors = "ENABLED"
# Explicit auth flows
explicit_auth_flows = [
"ALLOW_USER_PASSWORD_AUTH",
"ALLOW_REFRESH_TOKEN_AUTH",
"ALLOW_ADMIN_USER_PASSWORD_AUTH"
]
}
# Cognito User Pool Domain
resource "aws_cognito_user_pool_domain" "smart_domain" {
domain = "smart-fhir-auth"
user_pool_id = aws_cognito_user_pool.smart_pool.id
}
# Cognito Identity Pool (for AWS credentials)
resource "aws_cognito_identity_pool" "smart_identity" {
identity_pool_name = "smart-fhir-identity-pool"
allow_unauthenticated_identities = false
cognito_identity_providers {
client_id = aws_cognito_user_pool_client.smart_app.id
provider_name = aws_cognito_user_pool_domain.smart_domain.domain_name
server_side_token_check = true
}
}Terraform: API Gateway Authorizer
# API Gateway OAuth 2.0 Authorizer
resource "aws_apigatewayv2_authorizer" "oauth" {
api_id = aws_apigatewayv2_api.fhir_api.id
authorizer_type = "JWT"
name = "oauth-jwt-authorizer"
identity_sources = ["$request.header.Authorization"]
jwt_configuration {
audience = [aws_cognito_user_pool_client.smart_app.id]
issuer = "https://cognito-idp.ap-southeast-2.amazonaws.com/${aws_cognito_user_pool.smart_pool.id}"
}
}
# API Gateway Route with Authorizer
resource "aws_apigatewayv2_route" "patient_read" {
api_id = aws_apigatewayv2_api.fhir_api.id
route_key = "GET /Patient/{id}"
authorization_type = "JWT"
authorizer_id = aws_apigatewayv2_authorizer.oauth.id
target = "integrations/${aws_apigatewayv2_integration.patient_read.id}"
}
# Authorization scope requirement
resource "aws_apigatewayv2_authorizer" "oauth_scopes" {
api_id = aws_apigatewayv2_api.fhir_api.id
authorizer_type = "JWT"
name = "oauth-scope-authorizer"
identity_sources = ["$request.header.Authorization"]
jwt_configuration {
audience = [aws_cognito_user_pool_client.smart_app.id]
issuer = "https://cognito-idp.ap-southeast-2.amazonaws.com/${aws_cognito_user_pool.smart_pool.id}"
}
# Scope validation would be done in Lambda authorizer
}Cognito Limitations
Cognito doesn't natively support all SMART-specific claims. You may need Lambda triggers to add fhirUser claims to ID tokens.
JWT Tokens
JSON Web Tokens (JWT) are the standard token format for OAuth 2.0 and OIDC, containing claims about the user and authorization in a signed, compact format.
JWT Structure
JWTs consist of three parts separated by dots:
- Header: Algorithm and token type
- Payload: Claims (user info, scopes, expiration)
- Signature: Verification using secret or private key
JWT Example
JWT Token Structure
Structured JSON example rendered with depth controls for easier inspection.
Click on an annotation to highlight it in the JSON
Token Validation
Always validate JWT tokens before trusting:
- Verify signature using issuer's public key (JWKS)
- Check issuer (iss) matches expected value
- Verify audience (aud) matches your client ID
- Check expiration (exp) - token not expired
- Check not-before (nbf) if present
- Validate required claims present (sub, iat)
- Verify scopes for requested resource access
import { jwtVerify } from 'jose';
import { createRemoteJWKSet } from 'jose/jwks/remote';
const JWKS_URI = 'https://cognito-idp.ap-southeast-2.amazonaws.com/${USER_POOL_ID}/.well-known/jwks.json';
const ISSUER = `https://cognito-idp.ap-southeast-2.amazonaws.com/${USER_POOL_ID}`;
export async function validateToken(token: string, audience: string) {
const JWKS = createRemoteJWKSet(new URL(JWKS_URI));
try {
const { payload } = await jwtVerify(token, JWKS, {
issuer: ISSUER,
audience: audience,
});
return { valid: true, claims: payload };
} catch (error) {
return { valid: false, error: error.message };
}
}Well-Known URI & Discovery
The Well-Known URI provides standardized discovery of OAuth/OIDC server configuration, enabling clients to dynamically configure without hardcoding endpoints.
OIDC Discovery Endpoint
The OIDC configuration is available at:
GET /.well-known/openid-configuration HTTP/1.1
Host: cognito-idp.ap-southeast-2.amazonaws.com
Response:OIDC Configuration Response
Structured JSON example rendered with depth controls for easier inspection.
Click on an annotation to highlight it in the JSON
FHIR CapabilityStatement
FHIR servers also advertise SMART capabilities in their CapabilityStatement:
SMART Security Extension in CapabilityStatement
Structured JSON example rendered with depth controls for easier inspection.
Click on an annotation to highlight it in the JSON
JWKS Endpoint
The JSON Web Key Set (JWKS) endpoint provides public keys for token verification:
GET /.well-known/jwks.json HTTP/1.1
Host: cognito-idp.ap-southeast-2.amazonaws.com
Response:
{
"keys": [{
"kty": "RSA",
"kid": "key-id-123",
"use": "sig",
"alg": "RS256",
"n": "base64-modulus",
"e": "AQAB"
}]
}Discovery Best Practice
Always fetch Well-Known URI at startup and cache the configuration. Handle endpoint changes gracefully for server maintenance.
SMART on FHIR End-to-End Flow
SMART on FHIR defines standardized launch and authorization flows enabling third-party applications to securely access FHIR resources within EHR systems or as standalone applications.
OAuth 2.0 Authorization Code Flow with PKCE
Complete authorization flow with PKCE security extension:
SMART authorization code flow with PKCE
Loading diagram...
EHR Launch vs Standalone Launch
SMART launch types comparison
| Characteristic | EHR Launch | Standalone Launch |
|---|---|---|
| Launch Origin | From within EHR interface | Independent (bookmarked URL, app store) |
| Patient Context | Provided by EHR (launch/patient) | App must handle patient selection |
| User Context | Inherited from EHR session | App must authenticate user |
| Launch Parameters | iss, launch, patient, encounter | iss (FHIR server URL) |
| Use Case | Clinician workflow integration | Patient-facing apps, population health |
Access Token Refresh Rotation
Proactive token refresh strategy to prevent API failures:
- Access tokens: Short-lived (1 hour typical)
- Refresh tokens: Long-lived (30 days typical)
- Refresh at 55 minutes (for 1-hour access tokens)
- Rotate refresh tokens on each use (refresh token rotation)
- Handle 401 Unauthorized with immediate refresh
- Implement exponential backoff for refresh failures
- Store tokens securely (encrypted storage, httpOnly cookies)
class TokenManager {
private accessToken: string | null = null;
private refreshToken: string | null = null;
private tokenExpiry: number = 0;
async getValidToken(): Promise<string> {
// Refresh if token expires within 5 minutes
if (Date.now() >= this.tokenExpiry - 300000) {
await this.refreshAccessToken();
}
return this.accessToken!;
}
private async refreshAccessToken() {
const response = await fetch('/oauth2/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: this.refreshToken!,
client_id: CLIENT_ID
})
});
const tokens = await response.json();
this.accessToken = tokens.access_token;
this.refreshToken = tokens.refresh_token; // Rotation: new refresh token
this.tokenExpiry = Date.now() + (tokens.expires_in * 1000);
}
}Scope-Based Resource Access
Granular access control through SMART scopes:
SMART scope examples and access levels
| Scope | Context | Resources | Access Level |
|---|---|---|---|
| patient/Patient.read | Patient | Patient | Read-only |
| patient/Observation.read | Patient | Observation | Read-only |
| user/MedicationRequest.* | User | MedicationRequest | Read/Write |
| patient/*.read | Patient | All | Read-only |
| user/*.read | User | All | Read-only |
| launch/patient | Launch | N/A | Patient context |
| openid fhirUser | OIDC | ID Token | Identity |
Complete Flow Example
import { generateCodeChallenge, generateCodeVerifier } from './pkce';
class SMARTClient {
private codeVerifier: string;
private state: string;
constructor(private config: SMARTConfig) {
this.codeVerifier = generateCodeVerifier();
this.state = this.generateRandomState();
}
// Step 1: Build authorization URL
buildAuthUrl(launchParams: LaunchParams): string {
const codeChallenge = generateCodeChallenge(this.codeVerifier);
return `${this.config.authEndpoint}?
response_type=code&
client_id=${this.config.clientId}&
redirect_uri=${encodeURIComponent(this.config.redirectUri)}&
scope=${encodeURIComponent(this.config.scopes.join(' '))}&
state=${this.state}&
launch=${launchParams.launch}&
iss=${encodeURIComponent(launchParams.iss)}&
code_challenge=${codeChallenge}&
code_challenge_method=S256`;
}
// Step 2: Handle callback and exchange code for tokens
async handleCallback(callbackUrl: string): Promise<Tokens> {
const params = new URLSearchParams(new URL(callbackUrl).search);
const code = params.get('code');
const state = params.get('state');
// Validate state
if (state !== this.state) {
throw new Error('Invalid state parameter');
}
// Exchange code for tokens
const response = await fetch(this.config.tokenEndpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
code: code!,
redirect_uri: this.config.redirectUri,
client_id: this.config.clientId,
code_verifier: this.codeVerifier
})
});
return await response.json();
}
// Step 3: Access FHIR resources with token
async fetchPatient(token: string, patientId: string): Promise<Patient> {
const response = await fetch(`${this.config.fhirEndpoint}/Patient/${patientId}`, {
headers: {
'Authorization': `Bearer ${token}`,
'Accept': 'application/fhir+json'
}
});
return await response.json();
}
private generateRandomState(): string {
return Math.random().toString(36).substring(2, 15);
}
}PKCE Security
PKCE (Proof Key for Code Exchange) prevents authorization code interception attacks. Mandatory for all SMART public clients (mobile apps, SPAs). Never skip PKCE in production.
AWS Integration
- Cognito User Pool: OAuth 2.0/OIDC authorization server
- Cognito App Client: SMART app registration with scopes
- API Gateway JWT Authorizer: Validate access tokens
- Lambda Authorizer: Extract fhirUser claim, enforce scopes
- CloudWatch Logs: Audit all authorization events
- Secrets Manager: Store client secrets securely
Summary & Key Takeaways
SMART on FHIR authorization uses OAuth 2.0 and OIDC to provide secure, granular access to FHIR resources. Amazon Cognito provides a managed identity solution for implementing SMART authentication.
Core Concepts Recap
- OAuth 2.0: Authorization framework with PKCE
- OIDC: Identity layer with ID tokens
- SMART Scopes: Granular FHIR resource access
- Cognito: Managed OAuth/OIDC identity provider
- JWT: Token format for access and ID tokens
- Well-Known URI: Server configuration discovery
Implementation Checklist
- Configure Cognito User Pool with OAuth/OIDC
- Enable authorization code flow with PKCE
- Define SMART scopes (fhir/user, fhir/patient)
- Configure callback URLs for SMART app
- Implement JWT validation in API Gateway
- Add fhirUser claims via Lambda triggers
- Configure Well-Known URI discovery
- Test EHR launch and standalone launch flows
Next Steps
After understanding SMART authorization, explore clinical terminologies (SNOMED CT-AU, AMT), real-world implementation patterns, and Connectathon testing.
Knowledge Check
Test your understanding with this quiz. You need to answer all questions correctly to mark this section as complete.