Security and Authorization

Section 12 of 18
67% complete

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

PracticeDescription
Use HTTPSAlways use TLS for all FHIR communications
Token validationValidate JWT tokens server-side
Scope enforcementEnforce minimum necessary scopes
Token rotationImplement refresh token rotation
Audit loggingLog all access to sensitive data
Rate limitingProtect against abuse
Input validationValidate all input to prevent injection

Learn more about FHIR security and SMART on FHIR:

Quiz: Security and Authorization

Question 1 of 5

What is SMART on FHIR?