Skip to content

Integrating with HAPI FHIR Servers

Learn how to implement UDAP authentication into an existing HAPI FHIR server using interceptors. This tutorial demonstrates the minimum requirements for UDAP support.

Scope of This Tutorial

This guide covers the basics of UDAP integration. Topics like scope validation, claims checking, and caching strategies are beyond this tutorial's scope.

Prerequisites

You need a PKCS#12 (.pfx/.p12) certificate file containing:

  • Public key
  • Private key

Don't have a certificate?

Implementation Overview

The implementation uses HAPI FHIR's interceptor framework to add UDAP support:

  1. Add JWT dependencies
  2. Implement UDAP Discovery endpoint (.well-known/udap)
  3. Add authentication interceptor for resource protection

Step 1: Add Package Dependencies

Add the Auth0 JWT libraries to your pom.xml:

pom.xml
<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>4.5.0</version>
</dependency>
<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>jwks-rsa</artifactId>
    <version>0.22.2</version>
</dependency>

Library Versions

Check Maven Central for the latest versions of these libraries.


Step 2: Create UDAP Discovery Endpoint

Implement the .well-known/udap endpoint required for UDAP Discovery.

Create the Interceptor Class

DiscoveryInterceptor.java
@Interceptor
public class DiscoveryInterceptor {

  @Hook(Pointcut.SERVER_INCOMING_REQUEST_PRE_PROCESSED)
  public boolean incomingRequestPreProcessed(
      HttpServletRequest theRequest, 
      HttpServletResponse theResponse) 
      throws UnrecoverableKeyException, KeyStoreException, 
             NoSuchAlgorithmException, CertificateException, IOException {
    // Implementation below
  }
}

Check Request Path

Only intercept requests to the UDAP discovery endpoint:

Filter for UDAP endpoint
if (!theRequest.getRequestURI().equals("/fhir/.well-known/udap")) {
  return true; // Continue normal processing
}

Configure Endpoints and Certificate

Variable Configuration

In a real implementation, use external configuration (environment variables, Spring properties) instead of hardcoded values.

Configuration variables
String securityServerBase = "https://udap-security.fast.hl7.org";
String authEndpoint = securityServerBase + "/connect/authorize";
String tokenEndpoint = securityServerBase + "/connect/token";
String userinfoEndpoint = securityServerBase + "/connect/userinfo";
String revocationEndpoint = securityServerBase + "/connect/revocation";
String registrationEndpoint = securityServerBase + "/connect/register";

String certFile = System.getProperty("user.home") + "/cert-localhost8080.pfx";
String certPass = "udap-test";
String fhirBase = "http://localhost:8080/fhir";

Load Certificate and Keys

Load PKCS#12 certificate
FileInputStream stream = new FileInputStream(ResourceUtils.getFile(certFile));
KeyStore ks = KeyStore.getInstance("pkcs12");
ks.load(stream, certPass.toCharArray());
String alias = ks.aliases().nextElement();

X509Certificate certificate = (X509Certificate) ks.getCertificate(alias);
RSAPublicKey publicKey = (RSAPublicKey) certificate.getPublicKey();
RSAPrivateKey privateKey = (RSAPrivateKey) ks.getKey(alias, certPass.toCharArray());

Create and Sign JWT Metadata

The UDAP spec requires RS256 for signing discovery metadata:

Create signed metadata JWT
Algorithm algorithm = Algorithm.RSA256(publicKey, privateKey);
String signedMetadata = JWT.create()
    .withHeader(Map.of(
        "alg", algorithm.getName(),
        "x5c", new String[] { Base64.getEncoder().encodeToString(certificate.getEncoded()) }))
    .withIssuer(fhirBase)
    .withSubject(fhirBase)
    .withIssuedAt(Date.from(Instant.now()))
    .withExpiresAt(Date.from(Instant.now().plusMillis(86400000)))
    .withJWTId(UUID.randomUUID().toString())
    .withClaim("authorization_endpoint", authEndpoint)
    .withClaim("token_endpoint", tokenEndpoint)
    .withClaim("registration_endpoint", registrationEndpoint)
    .sign(algorithm);

Build Discovery Response

Create the JSON response following the UDAP metadata specification:

Build discovery metadata
Gson gson = new Gson();
JsonObject discoveryResponse = new JsonObject();
discoveryResponse.add("udap_versions_supported", gson.toJsonTree(List.of("1")));
discoveryResponse.add("udap_profiles_supported", 
    gson.toJsonTree(List.of("udap_dcr", "udap_authn", "udap_authz")));
discoveryResponse.add("udap_authorization_extensions_supported", 
    gson.toJsonTree(List.of("hl7-b2b")));
discoveryResponse.add("udap_authorization_extensions_required", 
    gson.toJsonTree(List.of("hl7-b2b")));
discoveryResponse.add("udap_certifications_supported", 
    gson.toJsonTree(List.of("https://www.example.com/udap/profiles/example-certification")));
discoveryResponse.add("udap_certifications_required", 
    gson.toJsonTree(List.of("https://www.example.com/udap/profiles/example-certification")));
discoveryResponse.add("grant_types_supported", 
    gson.toJsonTree(List.of("authorization_code", "refresh_token", "client_credentials")));
discoveryResponse.add("scopes_supported", 
    gson.toJsonTree(List.of("openid", "patient/*.read", "patient/*.rs", 
                            "user/*.read", "user/*.rs", "system/*.read", "system/*.rs")));
discoveryResponse.addProperty("authorization_endpoint", authEndpoint);
discoveryResponse.addProperty("token_endpoint", tokenEndpoint);
discoveryResponse.addProperty("userinfo_endpoint", userinfoEndpoint);
discoveryResponse.addProperty("revocation_endpoint", revocationEndpoint);
discoveryResponse.add("token_endpoint_auth_methods_supported", 
    gson.toJsonTree(List.of("private_key_jwt")));
discoveryResponse.add("token_endpoint_auth_signing_alg_values_supported", 
    gson.toJsonTree(List.of("ES256", "ES384", "RS256", "RS384")));
discoveryResponse.addProperty("registration_endpoint", registrationEndpoint);
discoveryResponse.add("registration_endpoint_jwt_signing_alg_values_supported", 
    gson.toJsonTree(List.of("ES256", "ES384", "RS256", "RS384")));
discoveryResponse.addProperty("signed_metadata", signedMetadata);

Metadata Model Class

In practice, you should probably create a dedicated metadata class using Spring REST patterns instead of building the JSON manually.

Return Response

Send the JSON response and stop further request processing:

Send response
theResponse.setContentType("application/json");
theResponse.getWriter().write(discoveryResponse.toString());
theResponse.setStatus(200);
theResponse.getWriter().close();

return false; // Stop further processing

Register the Interceptor

In your HAPI starter Application class:

Application.java
public ServletRegistrationBean hapiServletRegistration(RestfulServer restfulServer) {
  // ... existing code ...
  restfulServer.registerInterceptor(new DiscoveryInterceptor());
  // ... existing code ...
}

Step 3: Protect Resource Endpoints

Create an interceptor to validate access tokens on all endpoints except public ones.

Create the Interceptor Class

AuthInterceptor.java
@Interceptor
public class AuthInterceptor {

  @Hook(Pointcut.SERVER_INCOMING_REQUEST_POST_PROCESSED)
  public boolean incomingRequestPostProcessed(
      RequestDetails details, 
      HttpServletRequest request, 
      HttpServletResponse response) throws Exception {
    // Implementation below
  }
}

Configure Security Settings

Security configuration
String issuer = "https://udap-security.fast.hl7.org";
String jwksUri = issuer + "/.well-known/openid-configuration/jwks";
List<String> publicEndpoints = List.of("/fhir/metadata", "/fhir/.well-known/udap");

Variable Configuration

Again, in an actual implementation, use external configuration (environment variables, Spring properties) instead of hardcoded values.

Allow Public Endpoints

Check for public endpoints
if (publicEndpoints.contains(request.getRequestURI())) {
  return true; // Allow unauthenticated access
}

Validate Authorization Header

Check auth header
String authHeader = request.getHeader(Constants.HEADER_AUTHORIZATION);
if (authHeader == null || authHeader.isEmpty()
    || !authHeader.startsWith(Constants.HEADER_AUTHORIZATION_VALPREFIX_BEARER)) {
  throw new AuthenticationException("Missing or invalid Authorization header");
}

Extract and Verify Issuer

Verify token issuer
String token = authHeader.substring(Constants.HEADER_AUTHORIZATION_VALPREFIX_BEARER.length()).trim();

DecodedJWT decodedJWT = JWT.decode(token);
if (!decodedJWT.getIssuer().equals(issuer)) {
  throw new JWTVerificationException(
      "Invalid issuer: Expected \"" + issuer + "\" but received \"" + decodedJWT.getIssuer() + "\"");
}

Retrieve Public Key from JWKS

Implement Caching

In production, consider strategies to cache the JWKS keys to avoid fetching them on every request.

Get public key from JWKS
JwkProvider jwkProvider = new UrlJwkProvider(new URL(jwksUri));
Jwk jwk = jwkProvider.get(decodedJWT.getKeyId());
RSAPublicKey rsaPublicKey = (RSAPublicKey) jwk.getPublicKey();

if (rsaPublicKey == null) {
  throw new JWTVerificationException("Could not determine public key");
}

Verify Token Signature

Verify JWT signature
Algorithm algorithm = Algorithm.RSA256(rsaPublicKey, null);
JWTVerifier verifier = JWT.require(algorithm)
    .withIssuer(issuer)
    .build();

DecodedJWT verifiedJwt;
try {
  verifiedJwt = verifier.verify(token);
} catch (JWTVerificationException e) {
  throw new AuthenticationException("Token verification failed: " + e.getMessage());
}

return verifiedJwt != null;

Register the Interceptor

In your HAPI starter Application class:

Application.java
public ServletRegistrationBean hapiServletRegistration(RestfulServer restfulServer) {
  // ... existing code ...
  restfulServer.registerInterceptor(new AuthInterceptor());
  // ... existing code ...
}

Testing Your Integration

After implementing both interceptors:

  1. Test Discovery: curl http://localhost:8080/fhir/.well-known/udap
  2. Test Protected Resource: Try accessing a FHIR resource without a token (should fail)
  3. Test with Token: Use the FAST Security server to register a client and obtain a token. Consider using the sandbox example app in examples/sandbox/ for testing as it is already configured to work in a local environment.

Next Steps

  • Implement scope validation for fine-grained access control
  • Add claims processing for user/patient context
  • Implement JWKS key caching for better performance
  • Add comprehensive error handling and logging