Skip to content

Conversation

@mikaelweave
Copy link
Contributor

@mikaelweave mikaelweave commented Dec 3, 2025

Description

Implements RFC 7662 token introspection endpoint at /connect/introspect for SMART on FHIR server with swapple support for the introspection endpoint for alternate SMART configurations.

Key Features:

  • RFC 7662 compliant introspection endpoint supporting both OpenIddict (development) and external IdP tokens
  • Service abstraction pattern (ITokenIntrospectionService) enabling alternate authentication patterns
  • SMART on FHIR v1/v2 claim support (patient, fhirUser, raw_scope)
  • Bearer token authentication required via [Authorize] attribute
  • Integrated with existing audit logging infrastructure
  • Handles issuer variations (with/without trailing slash) for OpenIddict compatibility

Related issues

Addresses AB#174822

Testing

  • Unit Tests (12 tests - all passing):
  • Manual Testing

Test Coverage:

  • Controller layer: Parameter validation, HTTP status codes
  • Service layer: Token parsing, validation, claim extraction
  • Integration: Audit logging, authentication filters

FHIR Team Checklist

  • Update the title of the PR to be succinct and less than 65 characters
  • Add a milestone to the PR for the sprint that it is merged (i.e. add S47)
  • Tag the PR with the type of update: Bug, Build, Dependencies, Enhancement, New-Feature or Documentation
  • Tag the PR with Open source, Azure API for FHIR (CosmosDB or common code) or Azure Healthcare APIs (SQL or common code) to specify where this change is intended to be released.
  • Tag the PR with Schema Version backward compatible or Schema Version backward incompatible or Schema Version unchanged if this adds or updates Sql script which is/is not backward compatible with the code.
  • When changing or adding behavior, if your code modifies the system design or changes design assumptions, please create and include an ADR.
  • CI is green before merge Build Status
  • Review squash-merge requirements

Semver Change (docs)

Patch|Skip|Feature|Breaking (reason)

@mikaelweave mikaelweave changed the title Personal/mikaelw/smart token introspection endpoint SMART on FHIR Token Introspection Endpoint Dec 3, 2025
Updated `DefaultTokenIntrospectionService` to use `IHttpClientFactory` for managing `HttpClient` instances and initialized a shared `ConfigurationManager` for OpenID Connect configurations. Removed inline `ConfigurationManager` instantiation in token validation logic for consistency.

Enhanced `TokenIntrospectionControllerTests` by mocking `IHttpClientFactory` with `NSubstitute` to support the updated service constructor. Refactored `TokenIntrospectionTests` to improve handling of unauthenticated requests, added skipping logic for in-process test servers, and leveraged existing test infrastructure.

Removed `[Consumes]` attribute from `TokenIntrospectionController` to simplify content type handling. Replaced synchronous calls with asynchronous token validation to align with best practices. Added logging and validation for `httpClientFactory` dependency. Updated namespaces across files to support new functionality.
The test `GivenContentTypeNotFormEncoded_WhenIntrospecting_ThenReturnsUnsupportedMediaType` was removed from `TokenIntrospectionTests.cs`.

This test validated that the introspection endpoint returned `UnsupportedMediaType` when the content type was not `application/x-www-form-urlencoded` (per RFC 7662). Its removal suggests that this behavior is no longer relevant or required in the codebase.

Other tests, such as `GivenMultipleValidTokens_WhenIntrospecting_ThenEachReturnsCorrectClaims`, remain unchanged.
@mikaelweave mikaelweave marked this pull request as ready for review December 8, 2025 17:25
@mikaelweave mikaelweave requested a review from a team as a code owner December 8, 2025 17:25
Refactored `ValidateFormatParametersAttribute` to improve modularity by introducing `ShouldIgnoreValidation` for skipping validation on specific paths (e.g., `/CustomError`). Enhanced `Content-Type` validation for `POST`, `PUT`, and `PATCH` requests with better error handling for unsupported or missing headers.

Updated `TokenIntrospectionController` to remove the `[Authorize]` attribute, allowing unauthenticated access to `/connect/introspect`. Added `[Consumes("application/x-www-form-urlencoded")]` to specify the expected content type.

Removed a skipped test case and related code in `TokenIntrospectionTests` that validated unauthorized access to the token introspection endpoint, aligning with the updated authentication behavior.
@mikaelweave mikaelweave force-pushed the personal/mikaelw/smart-token-introspection-endpoint branch from 6b7008b to 5b23a53 Compare December 9, 2025 13:16
@mikaelweave
Copy link
Contributor Author

/azp run

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

mikaelweave and others added 3 commits December 9, 2025 11:05
…on local IDisposable

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Switch to TryGetValue for safer JSON field access in TokenIntrospectionTests, replacing ContainsKey/indexer usage. Also remove the unused _testFhirClient field for code clarity.
@mikaelweave mikaelweave added Azure API for FHIR Label denotes that the issue or PR is relevant to the Azure API for FHIR Azure Healthcare APIs Label denotes that the issue or PR is relevant to the FHIR service in the Azure Healthcare APIs labels Dec 9, 2025
@mikaelweave mikaelweave added this to the FY26\Q2\2Wk\2Wk12 milestone Dec 9, 2025
@mikaelweave mikaelweave added the Enhancement Enhancement on existing functionality. label Dec 9, 2025
mikaelweave and others added 4 commits December 9, 2025 14:51
Maintain and dispose HttpClient instance in tests to ensure proper resource management. The mock IHttpClientFactory now returns a dedicated HttpClient, which is disposed of in the test class's Dispose method.
'app_globalReaderUserApp_secret': $(app_globalReaderUserApp_secret)
'app_globalWriterUserApp_id': $(app_globalWriterUserApp_id)
'app_globalWriterUserApp_secret': $(app_globalWriterUserApp_secret)
'app_smartUserClient_id': $(app_smartUserClient_id)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

needed for E2E tests that use SMART client

serviceName = $webAppName
keyVaultName = "${{ parameters.keyVaultName }}".ToLower()
securityAuthenticationAuthority = "https://login.microsoftonline.com/$(tenant-id)"
securityAuthenticationAuthority = "https://sts.windows.net/$(tenant-id-guid)"
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Our test env has been using an invalid authority for ... not sure how long. I reuse the authority in the OSS service to check the token so I had to fix the authority.

// If the request is a put or post and has a content-type, check that it's supported
if (httpContext.Request.Method.Equals(HttpMethod.Post.Method, StringComparison.OrdinalIgnoreCase) ||
httpContext.Request.Method.Equals(HttpMethod.Put.Method, StringComparison.OrdinalIgnoreCase))
if (!ShouldIgnoreValidation(httpContext))
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This avoids format validation on token introspection endpoints

/// Default implementation of token introspection for OSS (single authority/audience).
/// PaaS can extend this class and override ValidateToken() to support multiple authorities.
/// </summary>
public class DefaultTokenIntrospectionService : ITokenIntrospectionService
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Note - this service can be overriden downstream for token validation

// Create mock HttpClientFactory that returns HttpClient for named client
var httpClientFactory = Substitute.For<IHttpClientFactory>();
httpClientFactory.CreateClient(DefaultTokenIntrospectionService.OidcConfigurationHttpClientName)
.Returns(new HttpClient());

Check warning

Code scanning / CodeQL

Missing Dispose call on local IDisposable Warning

Disposable 'HttpClient' is created but not disposed.

Copilot Autofix

AI 2 days ago

To fix the problem, ensure that the instantiated HttpClient object in the test setup is disposed of properly. Since it is registered as the return value for the substitute's CreateClient method and potentially used throughout the life of the TokenIntrospectionControllerTests test class, the best solution is to store the HttpClient instance in a field, and dispose of it in the test class's Dispose() method (since the class already implements IDisposable).

Make the following changes in src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Controllers/TokenIntrospectionControllerTests.cs:

  • Add a private field to hold the created HttpClient.
  • In the constructor, assign the new HttpClient to this field, and return it from the substitute as currently.
  • Implement or modify the Dispose() method of the test class so it disposes the field if it is not null.

No new imports are needed, as HttpClient and IDisposable are already imported.


Suggested changeset 1
src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Controllers/TokenIntrospectionControllerTests.cs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Controllers/TokenIntrospectionControllerTests.cs b/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Controllers/TokenIntrospectionControllerTests.cs
--- a/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Controllers/TokenIntrospectionControllerTests.cs
+++ b/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Controllers/TokenIntrospectionControllerTests.cs
@@ -37,6 +37,7 @@
         private readonly SigningCredentials _signingCredentials;
         private readonly string _issuer = "https://test-issuer.com";
         private readonly string _audience = "test-audience";
+        private readonly HttpClient _httpClient;
 
         public TokenIntrospectionControllerTests()
         {
@@ -63,8 +64,9 @@
 
             // Create mock HttpClientFactory that returns HttpClient for named client
             var httpClientFactory = Substitute.For<IHttpClientFactory>();
+            _httpClient = new HttpClient();
             httpClientFactory.CreateClient(DefaultTokenIntrospectionService.OidcConfigurationHttpClientName)
-                .Returns(new HttpClient());
+                .Returns(_httpClient);
 
             // Create introspection service
             _introspectionService = new DefaultTokenIntrospectionService(
@@ -406,5 +407,10 @@
         {
             _rsa?.Dispose();
         }
+        public void Dispose()
+        {
+            _httpClient?.Dispose();
+            _rsa?.Dispose();
+        }
     }
 }
EOF
@@ -37,6 +37,7 @@
private readonly SigningCredentials _signingCredentials;
private readonly string _issuer = "https://test-issuer.com";
private readonly string _audience = "test-audience";
private readonly HttpClient _httpClient;

public TokenIntrospectionControllerTests()
{
@@ -63,8 +64,9 @@

// Create mock HttpClientFactory that returns HttpClient for named client
var httpClientFactory = Substitute.For<IHttpClientFactory>();
_httpClient = new HttpClient();
httpClientFactory.CreateClient(DefaultTokenIntrospectionService.OidcConfigurationHttpClientName)
.Returns(new HttpClient());
.Returns(_httpClient);

// Create introspection service
_introspectionService = new DefaultTokenIntrospectionService(
@@ -406,5 +407,10 @@
{
_rsa?.Dispose();
}
public void Dispose()
{
_httpClient?.Dispose();
_rsa?.Dispose();
}
}
}
Copilot is powered by AI and may make mistakes. Always verify output.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Azure API for FHIR Label denotes that the issue or PR is relevant to the Azure API for FHIR Azure Healthcare APIs Label denotes that the issue or PR is relevant to the FHIR service in the Azure Healthcare APIs Enhancement Enhancement on existing functionality.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants