Skip to content

Add OAuth client_credentials grant support for FHIR server authentication#2348

Open
dionmcm wants to merge 9 commits into
hapifhir:masterfrom
dionmcm:feature/oauth-client-credentials
Open

Add OAuth client_credentials grant support for FHIR server authentication#2348
dionmcm wants to merge 9 commits into
hapifhir:masterfrom
dionmcm:feature/oauth-client-credentials

Conversation

@dionmcm
Copy link
Copy Markdown

@dionmcm dionmcm commented Mar 13, 2026

Purpose

The IG Publisher uses this code to authenticate to the configured FHIR terminology server. The existing code supports OAuth by providing a static token in configuration, however if the IG Publisher runs longer than the token expiry the IG Publisher process will fail. This was reported for users with a 30 minute token expiry policy they cannot change, unable to complete the IG Publisher run.

This code addresses this by adding configuration to supply client_id, client_secret, and token endpoint URL. The change uses that configuration to get a token instead of a statically configured token, monitors the token for expiry, and gets a new token when required.

Summary

  • Add OAuth 2.0 client_credentials grant type as a new authentication mode for configured FHIR servers, with automatic token acquisition, caching, and transparent refresh
  • Servers can be configured in fhir-settings.json with authenticationType: "client_credentials" plus clientId, clientSecret, and tokenEndpoint fields, or programmatically via withClientCredentials()
  • On 401/403 responses, the cached token is automatically invalidated and the request retried once with a fresh token
  • Refactor auth header resolution into a single resolveAuth() method in ManagedWebAccessorBase, eliminating 6 duplicated auth dispatch blocks across ManagedWebAccessor and ManagedFhirWebAccessor
  • Encapsulated retry logic into a shared executeWithTokenRetry() template method in the base class
  • Extract ParamNames constants for the validation HTTP handler, replacing scattered string literals

Test plan

  • HTTPTokenManagerTest — 8 tests covering token fetch, caching, expiry, invalidation, and error handling
  • ManagedWebAccessAuthTests — 14 tests covering all auth modes (basic, token, apikey, client_credentials) for both accessor types, including 401/403 retry
  • OAuthClientCredentialsIntegrationTest — 7 end-to-end tests with MockWebServer covering the full OAuth flow, transparent token refresh on expiry, and retry on 401/403
  • ManagedFhirWebAccessorTests — 2 tests for managed header construction
  • FhirSettingsTests — 7 tests including validation of required client_credentials fields
  • FhirValidatorHttpServiceTests — updated for ParamNames constant usage

dionmcm and others added 9 commits March 4, 2026 17:49
When the IG publisher runs for a long time, statically configured bearer tokens can expire. This adds support for configuring clientId, clientSecret, and tokenEndpoint so the library can manage OAuth tokens itself using the client_credentials grant.

 - Add clientId, clientSecret, tokenEndpoint fields to ServerDetailsPOJO
 - Create HTTPTokenManager for token lifecycle (fetch, cache, expiry)
 - Add client_credentials auth to ManagedFhirWebAccessor and ManagedWebAccessor
 - Add 401/403 retry with token invalidation for client_credentials servers
 - Add addServerAuthDetail() to ManagedWebAccess for programmatic configuration
 - Add validation in FhirSettings for required client_credentials fields
Implement OAuth 2.0 client_credentials flow for token management, enabling automatic fetching, caching, and refreshing of bearer tokens. Update `ManagedFhirWebAccessor`, `HTTPTokenManager`, and related classes to handle client_credentials authentication, including token invalidation and 401/403 retries. Extend `ServerDetailsPOJO` and authentication modes to support this functionality.
…non-JSON responses

Improve retry logic with debug and warning logs for OAuth token refresh scenarios, covering both 401 and 403 responses. Ensure clear error propagation by validating token endpoint responses and throwing IOExceptions for invalid or non-JSON content. Adjust tests accordingly.
@dotasek
Copy link
Copy Markdown
Collaborator

dotasek commented Mar 19, 2026

@dionmcm thank you for the contribution. The logic so far looks reasonable, but we recently merged breaking changes to ManagedWebAccess its related classes: #2341

Since you are most familiar with this change, could you adjust your PR or open a new one that uses the newer code?

It looks like most of your code will still work, but that the logic in org.hl7.fhir.utilities.http.ManagedWebAccessorBase#resolveAuth should be moved to

and

Please let me know if you have any questions or require assistance.

Copy link
Copy Markdown
Collaborator

@dotasek dotasek left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See PR comment regarding breaking changes to master. Otherwise, there are a few things I noticed on this quick review that should be considered.

I will re-review once the changes from master have been included.

return new CachedToken(accessToken, expiresAtMillis);
}

private static String readResponseBody(HttpURLConnection conn) throws IOException {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This deviates from our usage of org.hl7.fhir.utilities.FileUtilities#streamToBytes, which is used here for a similar purpose:

return new HTTPResult(url, c.getResponseCode(), c.getResponseMessage(), c.getRequestProperty("Content-Type"), FileUtilities.streamToBytes(c.getResponseCode() >= 400 ? c.getErrorStream() : c.getInputStream()));

headers.put("Api-Key", getToken());
}

private Map<String, String> newHeaders(String url) throws IOException {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When adjusting for the recent changes in master, keep in mind that this should definitely not be here. Auth should only be managed by IHTTPAuthenticationProvider implementations for now on.

@alexanderkiel
Copy link
Copy Markdown
Contributor

In the MII in Germany, we would be happy to use this client_credentials auth method to connect the validator to a terminology server.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants