From bba5de306aa2bc2fdcef9a5eaef3fe91932670bb Mon Sep 17 00:00:00 2001 From: Isao Yamauchi Date: Tue, 6 Jan 2026 13:36:22 -0800 Subject: [PATCH 1/2] Adding a configuration for SMART 3rd-party IDP. --- .../Configs/FhirServerConfiguration.cs | 2 + .../SmartIdentityProviderConfiguration.cs | 12 ++++++ .../GetSmartConfigurationHandler.cs | 15 +++++++- .../FhirServerServiceCollectionExtensions.cs | 1 + .../GetSmartConfigurationHandlerTests.cs | 37 ++++++++++++++++--- 5 files changed, 59 insertions(+), 8 deletions(-) create mode 100644 src/Microsoft.Health.Fhir.Core/Configs/SmartIdentityProviderConfiguration.cs diff --git a/src/Microsoft.Health.Fhir.Api/Configs/FhirServerConfiguration.cs b/src/Microsoft.Health.Fhir.Api/Configs/FhirServerConfiguration.cs index 8b494397cf..09a65e45e4 100644 --- a/src/Microsoft.Health.Fhir.Api/Configs/FhirServerConfiguration.cs +++ b/src/Microsoft.Health.Fhir.Api/Configs/FhirServerConfiguration.cs @@ -38,5 +38,7 @@ public class FhirServerConfiguration : IApiConfiguration public EncryptionConfiguration Encryption { get; } = new EncryptionConfiguration(); public ResourceManagerConfig ResourceManager { get; } = new ResourceManagerConfig(); + + public SmartIdentityProviderConfiguration SmartIdentityProvider { get; } = new SmartIdentityProviderConfiguration(); } } diff --git a/src/Microsoft.Health.Fhir.Core/Configs/SmartIdentityProviderConfiguration.cs b/src/Microsoft.Health.Fhir.Core/Configs/SmartIdentityProviderConfiguration.cs new file mode 100644 index 0000000000..91fc0b4f20 --- /dev/null +++ b/src/Microsoft.Health.Fhir.Core/Configs/SmartIdentityProviderConfiguration.cs @@ -0,0 +1,12 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. +// ------------------------------------------------------------------------------------------------- + +namespace Microsoft.Health.Fhir.Core.Configs +{ + public class SmartIdentityProviderConfiguration + { + public string Authority { get; set; } + } +} diff --git a/src/Microsoft.Health.Fhir.Core/Features/Conformance/GetSmartConfigurationHandler.cs b/src/Microsoft.Health.Fhir.Core/Features/Conformance/GetSmartConfigurationHandler.cs index 5bf0c544c5..908ad947ee 100644 --- a/src/Microsoft.Health.Fhir.Core/Features/Conformance/GetSmartConfigurationHandler.cs +++ b/src/Microsoft.Health.Fhir.Core/Features/Conformance/GetSmartConfigurationHandler.cs @@ -22,12 +22,17 @@ namespace Microsoft.Health.Fhir.Core.Features.Conformance public class GetSmartConfigurationHandler : IRequestHandler { private readonly SecurityConfiguration _securityConfiguration; + private readonly SmartIdentityProviderConfiguration _smartIdentityProviderConfiguration; - public GetSmartConfigurationHandler(IOptions securityConfigurationOptions) + public GetSmartConfigurationHandler( + IOptions securityConfigurationOptions, + IOptions smartIdentityProviderConfiguration) { EnsureArg.IsNotNull(securityConfigurationOptions?.Value, nameof(securityConfigurationOptions)); + EnsureArg.IsNotNull(smartIdentityProviderConfiguration?.Value, nameof(smartIdentityProviderConfiguration)); _securityConfiguration = securityConfigurationOptions.Value; + _smartIdentityProviderConfiguration = smartIdentityProviderConfiguration.Value; } public Task Handle(GetSmartConfigurationRequest request, CancellationToken cancellationToken) @@ -43,7 +48,7 @@ protected GetSmartConfigurationResponse Handle(GetSmartConfigurationRequest requ { try { - string baseEndpoint = _securityConfiguration.Authentication.Authority; + string baseEndpoint = GetAuthority(); Uri authorizationEndpoint = new Uri(baseEndpoint + "/authorize"); Uri tokenEndpoint = new Uri(baseEndpoint + "/token"); @@ -121,5 +126,11 @@ protected GetSmartConfigurationResponse Handle(GetSmartConfigurationRequest requ Core.Resources.SecurityConfigurationAuthorizationNotEnabled, HttpStatusCode.BadRequest); } + + private string GetAuthority() + { + return !string.IsNullOrEmpty(_smartIdentityProviderConfiguration.Authority) ? + _smartIdentityProviderConfiguration.Authority : _securityConfiguration.Authentication.Authority; + } } } diff --git a/src/Microsoft.Health.Fhir.Shared.Api/Registration/FhirServerServiceCollectionExtensions.cs b/src/Microsoft.Health.Fhir.Shared.Api/Registration/FhirServerServiceCollectionExtensions.cs index 1cf0623aa3..4c50cb3ca7 100644 --- a/src/Microsoft.Health.Fhir.Shared.Api/Registration/FhirServerServiceCollectionExtensions.cs +++ b/src/Microsoft.Health.Fhir.Shared.Api/Registration/FhirServerServiceCollectionExtensions.cs @@ -93,6 +93,7 @@ public static IFhirServerBuilder AddFhirServer( services.AddSingleton(Options.Options.Create(fhirServerConfiguration.Operations.Terminology)); services.AddSingleton(Options.Options.Create(fhirServerConfiguration.Audit)); services.AddSingleton(Options.Options.Create(fhirServerConfiguration.Bundle)); + services.AddSingleton(Options.Options.Create(fhirServerConfiguration.SmartIdentityProvider)); services.AddSingleton(); services.AddSingleton(provider => { diff --git a/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Conformance/GetSmartConfigurationHandlerTests.cs b/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Conformance/GetSmartConfigurationHandlerTests.cs index 2b25b8eefb..79e6131dae 100644 --- a/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Conformance/GetSmartConfigurationHandlerTests.cs +++ b/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Conformance/GetSmartConfigurationHandlerTests.cs @@ -36,7 +36,7 @@ public async Task GivenASmartConfigurationHandler_WhenSecurityConfigurationNotEn var securityConfiguration = new SecurityConfiguration(); securityConfiguration.Authorization.Enabled = false; - var handler = new GetSmartConfigurationHandler(Options.Create(securityConfiguration)); + var handler = new GetSmartConfigurationHandler(Options.Create(securityConfiguration), Options.Create(new SmartIdentityProviderConfiguration())); OperationFailedException e = await Assert.ThrowsAsync(() => handler.Handle(request, CancellationToken.None)); Assert.Equal(HttpStatusCode.BadRequest, e.ResponseStatusCode); @@ -53,7 +53,7 @@ public async Task GivenASmartConfigurationHandler_WhenSecurityConfigurationEnabl securityConfiguration.Authorization.Enabled = true; securityConfiguration.Authentication.Authority = baseEndpoint; - var handler = new GetSmartConfigurationHandler(Options.Create(securityConfiguration)); + var handler = new GetSmartConfigurationHandler(Options.Create(securityConfiguration), Options.Create(new SmartIdentityProviderConfiguration())); GetSmartConfigurationResponse response = await handler.Handle(request, CancellationToken.None); @@ -86,7 +86,7 @@ public async Task GivenASmartConfigurationHandler_WhenBaseEndpointIsInvalid_Then securityConfiguration.Authorization.Enabled = true; securityConfiguration.Authentication.Authority = baseEndpoint; - var handler = new GetSmartConfigurationHandler(Options.Create(securityConfiguration)); + var handler = new GetSmartConfigurationHandler(Options.Create(securityConfiguration), Options.Create(new SmartIdentityProviderConfiguration())); OperationFailedException exception = await Assert.ThrowsAsync(() => handler.Handle(request, CancellationToken.None)); Assert.Equal(HttpStatusCode.BadRequest, exception.ResponseStatusCode); @@ -113,7 +113,7 @@ public async Task GivenASmartConfigurationHandler_WhenOtherEndpointsAreSpecifire securityConfiguration.ManagementEndpoint = managementEndpoint; securityConfiguration.RevocationEndpoint = revocationEndpoint; - var handler = new GetSmartConfigurationHandler(Options.Create(securityConfiguration)); + var handler = new GetSmartConfigurationHandler(Options.Create(securityConfiguration), Options.Create(new SmartIdentityProviderConfiguration())); GetSmartConfigurationResponse response = await handler.Handle(request, CancellationToken.None); @@ -138,7 +138,7 @@ public async Task GivenASmartConfigurationHandler_WhenAadSmartOnFhirProxyEnabled securityConfiguration.Authentication.Authority = baseEndpoint; securityConfiguration.EnableAadSmartOnFhirProxy = true; - var handler = new GetSmartConfigurationHandler(Options.Create(securityConfiguration)); + var handler = new GetSmartConfigurationHandler(Options.Create(securityConfiguration), Options.Create(new SmartIdentityProviderConfiguration())); GetSmartConfigurationResponse response = await handler.Handle(request, CancellationToken.None); @@ -171,7 +171,7 @@ public async Task GivenASmartConfigurationHandler_WhenAadSmartOnFhirProxyDisable securityConfiguration.Authentication.Authority = "https://logon.onmicrosoft.com/guid"; securityConfiguration.EnableAadSmartOnFhirProxy = false; - var handler = new GetSmartConfigurationHandler(Options.Create(securityConfiguration)); + var handler = new GetSmartConfigurationHandler(Options.Create(securityConfiguration), Options.Create(new SmartIdentityProviderConfiguration())); GetSmartConfigurationResponse response = await handler.Handle(request, CancellationToken.None); @@ -192,5 +192,30 @@ public async Task GivenASmartConfigurationHandler_WhenAadSmartOnFhirProxyDisable Assert.NotNull(response.TokenEndpointAuthMethodsSupported); Assert.NotNull(response.ResponseTypesSupported); } + + [Theory] + [InlineData("https://smart.example.com/")] + [InlineData(null)] + public async Task GivenASmartConfigurationHandler_When3rdPartyIdpSpecified_ThenCorrectAuthorityEndpointShouldBeReturned(string authority) + { + var requestUri = new System.Uri("https://fhir.example.com/"); + var request = new GetSmartConfigurationRequest(requestUri); + + var baseUri = "https://logon.onmicrosoft.com/guid"; + var securityConfiguration = new SecurityConfiguration(); + securityConfiguration.Authorization.Enabled = true; + securityConfiguration.Authentication.Authority = baseUri; + + var smartIdentityProviderConfiguration = new SmartIdentityProviderConfiguration(); + smartIdentityProviderConfiguration.Authority = authority; + + var handler = new GetSmartConfigurationHandler(Options.Create(securityConfiguration), Options.Create(smartIdentityProviderConfiguration)); + + GetSmartConfigurationResponse response = await handler.Handle(request, CancellationToken.None); + + var expectedUri = !string.IsNullOrEmpty(authority) ? authority : baseUri; + Assert.Equal(expectedUri + "/authorize", response.AuthorizationEndpoint.ToString()); + Assert.Equal(expectedUri + "/token", response.TokenEndpoint.ToString()); + } } } From 93bf343b29e7d12f5fb24c3c80b22390eb187b0f Mon Sep 17 00:00:00 2001 From: Isao Yamauchi Date: Wed, 7 Jan 2026 13:43:29 -0800 Subject: [PATCH 2/2] Addressing reviewer's comments. --- .../Configs/SecurityConfiguration.cs | 6 ----- .../SmartIdentityProviderConfiguration.cs | 22 +++++++++++++++++++ .../GetSmartConfigurationHandler.cs | 9 ++++---- .../GetSmartConfigurationHandlerTests.cs | 16 ++++++++------ 4 files changed, 36 insertions(+), 17 deletions(-) diff --git a/src/Microsoft.Health.Fhir.Core/Configs/SecurityConfiguration.cs b/src/Microsoft.Health.Fhir.Core/Configs/SecurityConfiguration.cs index fcd0b5bda5..ad0aca8603 100644 --- a/src/Microsoft.Health.Fhir.Core/Configs/SecurityConfiguration.cs +++ b/src/Microsoft.Health.Fhir.Core/Configs/SecurityConfiguration.cs @@ -26,11 +26,5 @@ public class SecurityConfiguration public string ServicePrincipalClientId { get; set; } public AddAuthenticationLibraryMethod AddAuthenticationLibrary { get; set; } - - public string IntrospectionEndpoint { get; set; } - - public string ManagementEndpoint { get; set; } - - public string RevocationEndpoint { get; set; } } } diff --git a/src/Microsoft.Health.Fhir.Core/Configs/SmartIdentityProviderConfiguration.cs b/src/Microsoft.Health.Fhir.Core/Configs/SmartIdentityProviderConfiguration.cs index 91fc0b4f20..e5f4ad2d5e 100644 --- a/src/Microsoft.Health.Fhir.Core/Configs/SmartIdentityProviderConfiguration.cs +++ b/src/Microsoft.Health.Fhir.Core/Configs/SmartIdentityProviderConfiguration.cs @@ -5,8 +5,30 @@ namespace Microsoft.Health.Fhir.Core.Configs { + /// + /// Configuration for a third-party SMART identity provider. + /// public class SmartIdentityProviderConfiguration { + /// + /// Gets or sets the authority URL for the third-party identity provider. + /// This overrides the default authority from SecurityConfiguration when specified. + /// public string Authority { get; set; } + + /// + /// Gets or sets the introspection endpoint URL for token introspection. + /// + public string Introspection { get; set; } + + /// + /// Gets or sets the management endpoint URL for application management. + /// + public string Management { get; set; } + + /// + /// Gets or sets the revocation endpoint URL for token revocation. + /// + public string Revocation { get; set; } } } diff --git a/src/Microsoft.Health.Fhir.Core/Features/Conformance/GetSmartConfigurationHandler.cs b/src/Microsoft.Health.Fhir.Core/Features/Conformance/GetSmartConfigurationHandler.cs index 908ad947ee..6e63444270 100644 --- a/src/Microsoft.Health.Fhir.Core/Features/Conformance/GetSmartConfigurationHandler.cs +++ b/src/Microsoft.Health.Fhir.Core/Features/Conformance/GetSmartConfigurationHandler.cs @@ -110,9 +110,9 @@ protected GetSmartConfigurationResponse Handle(GetSmartConfigurationRequest requ grantTypesSupported, tokenEndpointAuthMethodsSupported, responseTypesSupported, - _securityConfiguration.IntrospectionEndpoint, - _securityConfiguration.ManagementEndpoint, - _securityConfiguration.RevocationEndpoint); + _smartIdentityProviderConfiguration.Introspection, + _smartIdentityProviderConfiguration.Management, + _smartIdentityProviderConfiguration.Revocation); } catch (Exception e) when (e is ArgumentNullException || e is UriFormatException) { @@ -129,8 +129,9 @@ protected GetSmartConfigurationResponse Handle(GetSmartConfigurationRequest requ private string GetAuthority() { - return !string.IsNullOrEmpty(_smartIdentityProviderConfiguration.Authority) ? + var authority = !string.IsNullOrEmpty(_smartIdentityProviderConfiguration.Authority) ? _smartIdentityProviderConfiguration.Authority : _securityConfiguration.Authentication.Authority; + return authority?.TrimEnd('/'); } } } diff --git a/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Conformance/GetSmartConfigurationHandlerTests.cs b/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Conformance/GetSmartConfigurationHandlerTests.cs index 79e6131dae..d3a62a6eee 100644 --- a/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Conformance/GetSmartConfigurationHandlerTests.cs +++ b/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Conformance/GetSmartConfigurationHandlerTests.cs @@ -97,7 +97,7 @@ public async Task GivenASmartConfigurationHandler_WhenBaseEndpointIsInvalid_Then [InlineData(null, "https://ehr.example.com/user/manage", null)] [InlineData(null, null, "https://ehr.example.com/user/revoke")] [InlineData("https://ehr.example.com/user/introspect", "https://ehr.example.com/user/manage", "https://ehr.example.com/user/revoke")] - public async Task GivenASmartConfigurationHandler_WhenOtherEndpointsAreSpecifired_ThenSmartConfigurationShouldContainsOtherEndpoints( + public async Task GivenASmartConfigurationHandler_WhenOtherEndpointsAreSpecified_ThenSmartConfigurationShouldContainOtherEndpoints( string introspectionEndpoint, string managementEndpoint, string revocationEndpoint) @@ -109,11 +109,13 @@ public async Task GivenASmartConfigurationHandler_WhenOtherEndpointsAreSpecifire var securityConfiguration = new SecurityConfiguration(); securityConfiguration.Authorization.Enabled = true; securityConfiguration.Authentication.Authority = baseEndpoint; - securityConfiguration.IntrospectionEndpoint = introspectionEndpoint; - securityConfiguration.ManagementEndpoint = managementEndpoint; - securityConfiguration.RevocationEndpoint = revocationEndpoint; - var handler = new GetSmartConfigurationHandler(Options.Create(securityConfiguration), Options.Create(new SmartIdentityProviderConfiguration())); + var smartIdentityProviderConfiguration = new SmartIdentityProviderConfiguration(); + smartIdentityProviderConfiguration.Introspection = introspectionEndpoint; + smartIdentityProviderConfiguration.Management = managementEndpoint; + smartIdentityProviderConfiguration.Revocation = revocationEndpoint; + + var handler = new GetSmartConfigurationHandler(Options.Create(securityConfiguration), Options.Create(smartIdentityProviderConfiguration)); GetSmartConfigurationResponse response = await handler.Handle(request, CancellationToken.None); @@ -214,8 +216,8 @@ public async Task GivenASmartConfigurationHandler_When3rdPartyIdpSpecified_ThenC GetSmartConfigurationResponse response = await handler.Handle(request, CancellationToken.None); var expectedUri = !string.IsNullOrEmpty(authority) ? authority : baseUri; - Assert.Equal(expectedUri + "/authorize", response.AuthorizationEndpoint.ToString()); - Assert.Equal(expectedUri + "/token", response.TokenEndpoint.ToString()); + Assert.Equal(expectedUri.TrimEnd('/') + "/authorize", response.AuthorizationEndpoint.ToString()); + Assert.Equal(expectedUri.TrimEnd('/') + "/token", response.TokenEndpoint.ToString()); } } }