OAuth 2.0 Integration
FHIR servers typically use OAuth 2.0 for authorization. OAuth 2.0 provides secure, token-based access control that separates authentication from authorization. For healthcare applications, proper authentication protects sensitive patient data and ensures regulatory compliance with HIPAA and other standards.
Basic Authentication
Basic authentication transmits username and password with each request. While simple to implement, it is only suitable for development or internal systems. Always use HTTPS to protect credentials in transit. For production systems, prefer OAuth 2.0 with bearer tokens.
import ca.uhn.fhir.rest.client.interceptor.BasicAuthInterceptor;
public class BasicAuthExample {
private static final FhirContext ctx = FhirContext.forR4();
public static IGenericClient createClientWithBasicAuth(
String serverBase, String username, String password) {
IGenericClient client = ctx.newRestfulGenericClient(serverBase);
// Add basic auth interceptor
client.registerInterceptor(
new BasicAuthInterceptor(username, password));
return client;
}
}
Bearer Token Authentication
Bearer tokens provide stateless authentication where the token itself contains or references authorization information. Tokens are obtained from an OAuth 2.0 authorization server and included in the Authorization header. Bearer tokens are preferred for production because they can be scoped, time-limited, and revoked without changing user credentials.
import ca.uhn.fhir.rest.client.interceptor.BearerTokenAuthInterceptor;
public class BearerTokenAuthExample {
private static final FhirContext ctx = FhirContext.forR4();
public static IGenericClient createClientWithBearerToken(
String serverBase, String accessToken) {
IGenericClient client = ctx.newRestfulGenericClient(serverBase);
// Add bearer token interceptor
client.registerInterceptor(
new BearerTokenAuthInterceptor(accessToken));
return client;
}
public static void demonstrateTokenUsage() {
String serverBase = "https://fhir.example.com/baseR4";
String token = "eyJhbGciOiJSUzI1NiIsInR5cCI..."; // Your access token
IGenericClient client = createClientWithBearerToken(serverBase, token);
// Now all requests will include Authorization: Bearer <token>
Patient patient = client.read()
.resource(Patient.class)
.withId("123")
.execute();
}
}
OAuth 2.0 Token Acquisition
Acquire access tokens using the OAuth 2.0 client credentials grant for backend service-to-service communication. This flow is appropriate when there is no end user involved, such as batch processing or system integrations. The client authenticates with its ID and secret to receive an access token with specified scopes.
import com.nimbusds.oauth2.sdk.*;
import com.nimbusds.oauth2.sdk.auth.*;
import com.nimbusds.oauth2.sdk.token.*;
import java.net.URI;
public class OAuth2TokenExample {
public static String getAccessToken(
String tokenEndpoint,
String clientId,
String clientSecret,
String scope) throws Exception {
// Build token request
URI tokenUri = new URI(tokenEndpoint);
ClientAuthentication clientAuth = new ClientSecretBasic(
new ClientID(clientId),
new Secret(clientSecret)
);
Scope oauthScope = new Scope(scope);
TokenRequest request = new TokenRequest(
tokenUri,
clientAuth,
new ClientCredentialsGrant(),
oauthScope
);
// Execute request
TokenResponse response = TokenResponse.parse(request.toHTTPRequest().send());
if (response.indicatesSuccess()) {
AccessTokenResponse successResponse = response.toSuccessResponse();
return successResponse.getTokens().getAccessToken().getValue();
} else {
TokenErrorResponse errorResponse = response.toErrorResponse();
throw new RuntimeException("Token request failed: " +
errorResponse.getErrorObject().getDescription());
}
}
public static void main(String[] args) throws Exception {
String token = getAccessToken(
"https://auth.example.com/oauth2/token",
"my-client-id",
"my-client-secret",
"patient/*.read"
);
System.out.println("Access token: " + token);
}
}
SMART on FHIR
SMART on FHIR provides a standardized way for applications to integrate with EHR systems. SMART (Substitutable Medical Applications, Reusable Technologies) builds on OAuth 2.0 to add healthcare-specific features like launch context, patient selection, and standardized scopes. SMART enables app marketplaces where certified applications can work across different EHR systems.
Discovering SMART Endpoints
SMART servers advertise their authorization endpoints through the CapabilityStatement or a .well-known/smart-configuration endpoint. Discovery allows applications to dynamically configure themselves for different servers without hardcoding URLs. Always discover endpoints rather than assuming their locations.
import org.hl7.fhir.r4.model.CapabilityStatement;
public class SmartDiscoveryExample {
private static final FhirContext ctx = FhirContext.forR4();
public static SmartEndpoints discoverSmartEndpoints(String fhirServerBase) {
IGenericClient client = ctx.newRestfulGenericClient(fhirServerBase);
// Get capability statement
CapabilityStatement capabilityStatement = client.capabilities()
.ofType(CapabilityStatement.class)
.execute();
SmartEndpoints endpoints = new SmartEndpoints();
// Extract security extension
for (CapabilityStatement.CapabilityStatementRestComponent rest :
capabilityStatement.getRest()) {
if (rest.hasSecurity()) {
for (Extension ext : rest.getSecurity().getExtension()) {
if (ext.getUrl().contains("oauth-uris")) {
for (Extension subExt : ext.getExtension()) {
String url = subExt.getUrl();
String value = ((UriType) subExt.getValue()).getValue();
if ("authorize".equals(url)) {
endpoints.setAuthorizeUrl(value);
} else if ("token".equals(url)) {
endpoints.setTokenUrl(value);
} else if ("register".equals(url)) {
endpoints.setRegisterUrl(value);
}
}
}
}
}
}
return endpoints;
}
static class SmartEndpoints {
private String authorizeUrl;
private String tokenUrl;
private String registerUrl;
// Getters and setters
public String getAuthorizeUrl() { return authorizeUrl; }
public void setAuthorizeUrl(String url) { this.authorizeUrl = url; }
public String getTokenUrl() { return tokenUrl; }
public void setTokenUrl(String url) { this.tokenUrl = url; }
public String getRegisterUrl() { return registerUrl; }
public void setRegisterUrl(String url) { this.registerUrl = url; }
}
}
SMART App Launch Flow
The SMART App Launch flow guides users through authorization with patient or provider context. In an EHR launch, the application receives a launch parameter containing context about the current patient and encounter. Standalone launches allow applications to request patient selection during authorization. The flow follows OAuth 2.0 authorization code grant with SMART extensions.
public class SmartAppLaunchExample {
private String clientId;
private String clientSecret;
private String redirectUri;
private SmartEndpoints endpoints;
public SmartAppLaunchExample(String clientId, String clientSecret,
String redirectUri, SmartEndpoints endpoints) {
this.clientId = clientId;
this.clientSecret = clientSecret;
this.redirectUri = redirectUri;
this.endpoints = endpoints;
}
// Step 1: Build authorization URL
public String buildAuthorizationUrl(String scope, String state, String launch) {
StringBuilder url = new StringBuilder(endpoints.getAuthorizeUrl());
url.append("?response_type=code");
url.append("&client_id=").append(URLEncoder.encode(clientId, StandardCharsets.UTF_8));
url.append("&redirect_uri=").append(URLEncoder.encode(redirectUri, StandardCharsets.UTF_8));
url.append("&scope=").append(URLEncoder.encode(scope, StandardCharsets.UTF_8));
url.append("&state=").append(state);
url.append("&aud=").append(URLEncoder.encode(fhirServerBase, StandardCharsets.UTF_8));
if (launch != null) {
url.append("&launch=").append(launch);
}
return url.toString();
}
// Step 2: Exchange authorization code for tokens
public TokenResponse exchangeCodeForTokens(String authorizationCode) throws Exception {
// Build token request
Map<String, String> params = new HashMap<>();
params.put("grant_type", "authorization_code");
params.put("code", authorizationCode);
params.put("redirect_uri", redirectUri);
params.put("client_id", clientId);
HttpClient httpClient = HttpClient.newHttpClient();
StringBuilder body = new StringBuilder();
for (Map.Entry<String, String> entry : params.entrySet()) {
if (body.length() > 0) body.append("&");
body.append(URLEncoder.encode(entry.getKey(), StandardCharsets.UTF_8));
body.append("=");
body.append(URLEncoder.encode(entry.getValue(), StandardCharsets.UTF_8));
}
String credentials = Base64.getEncoder()
.encodeToString((clientId + ":" + clientSecret).getBytes());
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(endpoints.getTokenUrl()))
.header("Content-Type", "application/x-www-form-urlencoded")
.header("Authorization", "Basic " + credentials)
.POST(HttpRequest.BodyPublishers.ofString(body.toString()))
.build();
HttpResponse<String> response = httpClient.send(request,
HttpResponse.BodyHandlers.ofString());
// Parse JSON response
ObjectMapper mapper = new ObjectMapper();
return mapper.readValue(response.body(), TokenResponse.class);
}
static class TokenResponse {
public String access_token;
public String token_type;
public int expires_in;
public String scope;
public String refresh_token;
public String patient; // SMART context
public String encounter; // SMART context
}
}
SMART Scopes
SMART defines specific scope patterns for FHIR access. Scopes control what data an application can access and what operations it can perform. The scope format is context/resourceType.permission where context is patient, user, or system. Request only the minimum scopes needed for your application to follow the principle of least privilege.
public class SmartScopesExample {
// Common SMART scopes
public static final String PATIENT_READ_ALL = "patient/*.read";
public static final String PATIENT_WRITE_ALL = "patient/*.write";
public static final String USER_READ_ALL = "user/*.read";
public static final String LAUNCH_PATIENT = "launch/patient";
public static final String LAUNCH_ENCOUNTER = "launch/encounter";
public static final String OPENID = "openid";
public static final String FHIR_USER = "fhirUser";
public static final String OFFLINE_ACCESS = "offline_access";
// Resource-specific scopes
public static final String PATIENT_READ_PATIENT = "patient/Patient.read";
public static final String PATIENT_READ_OBSERVATION = "patient/Observation.read";
public static final String PATIENT_READ_CONDITION = "patient/Condition.read";
public static final String PATIENT_READ_MEDICATION = "patient/MedicationRequest.read";
public static String buildScopeString(String... scopes) {
return String.join(" ", scopes);
}
public static void main(String[] args) {
// Build scope for patient portal app
String patientPortalScopes = buildScopeString(
LAUNCH_PATIENT,
PATIENT_READ_ALL,
OPENID,
FHIR_USER,
OFFLINE_ACCESS
);
System.out.println("Patient Portal Scopes: " + patientPortalScopes);
// Build scope for clinical app
String clinicalAppScopes = buildScopeString(
"launch",
USER_READ_ALL,
"user/*.write",
OPENID,
FHIR_USER
);
System.out.println("Clinical App Scopes: " + clinicalAppScopes);
}
}
Secure Client Configuration
Configure HAPI FHIR client for secure connections. Production FHIR applications must use TLS (HTTPS) for all communications. For enhanced security, configure custom trust stores for certificate validation or implement mutual TLS (mTLS) where both client and server present certificates. Always verify server certificates and consider certificate pinning for high-security environments.
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;
import java.security.KeyStore;
public class SecureClientConfiguration {
private static final FhirContext ctx = FhirContext.forR4();
public static IGenericClient createSecureClient(
String serverBase,
String keystorePath,
String keystorePassword) throws Exception {
// Load keystore
KeyStore keyStore = KeyStore.getInstance("PKCS12");
try (FileInputStream fis = new FileInputStream(keystorePath)) {
keyStore.load(fis, keystorePassword.toCharArray());
}
// Create SSL context
TrustManagerFactory tmf = TrustManagerFactory.getInstance(
TrustManagerFactory.getDefaultAlgorithm());
tmf.init(keyStore);
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, tmf.getTrustManagers(), null);
// Create HTTP client with SSL
SSLConnectionSocketFactory sslSocketFactory =
new SSLConnectionSocketFactory(sslContext);
CloseableHttpClient httpClient = HttpClientBuilder.create()
.setSSLSocketFactory(sslSocketFactory)
.build();
// Configure FHIR client factory
ctx.getRestfulClientFactory().setHttpClient(httpClient);
return ctx.newRestfulGenericClient(serverBase);
}
public static IGenericClient createClientWithMTLS(
String serverBase,
String clientCertPath,
String clientKeyPath,
String caCertPath) throws Exception {
// Mutual TLS configuration
// Load client certificate and key
// Configure for client authentication
// This is a simplified example - production code would be more complex
SSLContext sslContext = SSLContext.getInstance("TLS");
// Configure with client cert and CA cert
CloseableHttpClient httpClient = HttpClientBuilder.create()
.setSSLContext(sslContext)
.build();
ctx.getRestfulClientFactory().setHttpClient(httpClient);
return ctx.newRestfulGenericClient(serverBase);
}
}
Token Refresh
Handle OAuth 2.0 token refresh automatically. Access tokens expire for security reasons, typically within an hour. Refresh tokens allow obtaining new access tokens without re-authentication. Implement automatic refresh before token expiry to maintain seamless operation. Store refresh tokens securely as they provide long-term access to protected resources.
public class TokenRefreshInterceptor implements IClientInterceptor {
private String accessToken;
private String refreshToken;
private Instant tokenExpiry;
private final String tokenEndpoint;
private final String clientId;
private final String clientSecret;
public TokenRefreshInterceptor(String tokenEndpoint, String clientId,
String clientSecret, TokenResponse initialTokens) {
this.tokenEndpoint = tokenEndpoint;
this.clientId = clientId;
this.clientSecret = clientSecret;
updateTokens(initialTokens);
}
@Override
public void interceptRequest(IHttpRequest request) {
// Check if token needs refresh
if (Instant.now().isAfter(tokenExpiry.minusSeconds(60))) {
try {
refreshAccessToken();
} catch (Exception e) {
throw new RuntimeException("Token refresh failed", e);
}
}
// Add authorization header
request.addHeader("Authorization", "Bearer " + accessToken);
}
@Override
public void interceptResponse(IHttpResponse response) {
// Handle 401 responses - could trigger refresh
if (response.getStatus() == 401) {
// Token might be invalid - handle accordingly
}
}
private void refreshAccessToken() throws Exception {
HttpClient httpClient = HttpClient.newHttpClient();
String body = "grant_type=refresh_token" +
"&refresh_token=" + URLEncoder.encode(refreshToken, StandardCharsets.UTF_8) +
"&client_id=" + URLEncoder.encode(clientId, StandardCharsets.UTF_8);
String credentials = Base64.getEncoder()
.encodeToString((clientId + ":" + clientSecret).getBytes());
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(tokenEndpoint))
.header("Content-Type", "application/x-www-form-urlencoded")
.header("Authorization", "Basic " + credentials)
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse<String> response = httpClient.send(request,
HttpResponse.BodyHandlers.ofString());
if (response.statusCode() == 200) {
ObjectMapper mapper = new ObjectMapper();
TokenResponse tokens = mapper.readValue(response.body(), TokenResponse.class);
updateTokens(tokens);
} else {
throw new RuntimeException("Token refresh failed: " + response.body());
}
}
private void updateTokens(TokenResponse tokens) {
this.accessToken = tokens.access_token;
if (tokens.refresh_token != null) {
this.refreshToken = tokens.refresh_token;
}
this.tokenExpiry = Instant.now().plusSeconds(tokens.expires_in);
}
static class TokenResponse {
public String access_token;
public String refresh_token;
public int expires_in;
}
}
Security Best Practices
| Practice | Description |
|---|---|
| Use HTTPS | Always use TLS for all FHIR communications |
| Token validation | Validate JWT tokens server-side |
| Scope enforcement | Enforce minimum necessary scopes |
| Token rotation | Implement refresh token rotation |
| Audit logging | Log all access to sensitive data |
| Rate limiting | Protect against abuse |
| Input validation | Validate all input to prevent injection |
Related Articles
Learn more about FHIR security and SMART on FHIR:
- Building SMART on FHIR Apps using Java - Complete SMART app implementation
- Building SMART on FHIR Apps using .NET - .NET SMART app guide
- FHIR Programming using Java HAPI - Advanced Topics - Security best practices
- FHIR Programming using .NET - Advanced Topics - .NET security patterns
- HIPAA Healthcare Regulatory Compliance - Understanding healthcare compliance requirements
- Introduction to Post-Quantum Cryptography - Future-proofing security