diff --git a/src/Microsoft.Health.Fhir.Core/Configs/SecurityConfiguration.cs b/src/Microsoft.Health.Fhir.Core/Configs/SecurityConfiguration.cs index ad0aca8603..fcd0b5bda5 100644 --- a/src/Microsoft.Health.Fhir.Core/Configs/SecurityConfiguration.cs +++ b/src/Microsoft.Health.Fhir.Core/Configs/SecurityConfiguration.cs @@ -26,5 +26,11 @@ 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/Features/Conformance/GetSmartConfigurationHandler.cs b/src/Microsoft.Health.Fhir.Core/Features/Conformance/GetSmartConfigurationHandler.cs index 063ba9766a..be9eccdd10 100644 --- a/src/Microsoft.Health.Fhir.Core/Features/Conformance/GetSmartConfigurationHandler.cs +++ b/src/Microsoft.Health.Fhir.Core/Features/Conformance/GetSmartConfigurationHandler.cs @@ -66,7 +66,41 @@ protected GetSmartConfigurationResponse Handle(GetSmartConfigurationRequest requ "online_access", }; - return new GetSmartConfigurationResponse(authorizationEndpoint, tokenEndpoint, capabilities, scopesSupported); + ICollection codeChallengeMethodsSupported = new List + { + "S256", + }; + + ICollection grantTypesSupported = new List + { + "authorization_code", + "client_credentials", + }; + + ICollection tokenEndpointAuthMethodsSupported = new List + { + "client_secret_basic", + "client_secret_jwt", + "none", + }; + + ICollection responseTypesSupported = new List + { + "code", + }; + + return new GetSmartConfigurationResponse( + authorizationEndpoint, + tokenEndpoint, + capabilities, + scopesSupported, + codeChallengeMethodsSupported, + grantTypesSupported, + tokenEndpointAuthMethodsSupported, + responseTypesSupported, + _securityConfiguration.IntrospectionEndpoint, + _securityConfiguration.ManagementEndpoint, + _securityConfiguration.RevocationEndpoint); } catch (Exception e) when (e is ArgumentNullException || e is UriFormatException) { diff --git a/src/Microsoft.Health.Fhir.Core/Features/Operations/SmartConfigurationResult.cs b/src/Microsoft.Health.Fhir.Core/Features/Operations/SmartConfigurationResult.cs index 57a30b311f..843d78a116 100644 --- a/src/Microsoft.Health.Fhir.Core/Features/Operations/SmartConfigurationResult.cs +++ b/src/Microsoft.Health.Fhir.Core/Features/Operations/SmartConfigurationResult.cs @@ -26,7 +26,18 @@ public SmartConfigurationResult(Uri authorizationEndpoint, Uri tokenEndpoint, IC Capabilities = capabilities; } - public SmartConfigurationResult(Uri authorizationEndpoint, Uri tokenEndpoint, ICollection capabilities, ICollection scopesSupported) + public SmartConfigurationResult( + Uri authorizationEndpoint, + Uri tokenEndpoint, + ICollection capabilities, + ICollection scopesSupported, + ICollection codeChallengeMethodsSupported = null, + ICollection grantTypesSupported = null, + ICollection tokenEndpointAuthMethodsSupported = null, + ICollection responseTypesSupported = null, + string introspectionEndpoint = null, + string managementEndpoint = null, + string revocationEndpoint = null) { EnsureArg.IsNotNull(authorizationEndpoint, nameof(authorizationEndpoint)); EnsureArg.IsNotNull(tokenEndpoint, nameof(tokenEndpoint)); @@ -36,6 +47,13 @@ public SmartConfigurationResult(Uri authorizationEndpoint, Uri tokenEndpoint, IC TokenEndpoint = tokenEndpoint; Capabilities = capabilities; ScopesSupported = scopesSupported; + CodeChallengeMethodsSupported = codeChallengeMethodsSupported; + GrantTypesSupported = grantTypesSupported; + TokenEndpointAuthMethodsSupported = tokenEndpointAuthMethodsSupported; + ResponseTypesSupported = responseTypesSupported; + IntrospectionEndpoint = introspectionEndpoint; + ManagementEndpoint = managementEndpoint; + RevocationEndpoint = revocationEndpoint; } [JsonConstructor] @@ -43,10 +61,10 @@ public SmartConfigurationResult() { } - [JsonProperty("authorizationEndpoint")] + [JsonProperty("authorization_endpoint")] public Uri AuthorizationEndpoint { get; private set; } - [JsonProperty("tokenEndpoint")] + [JsonProperty("token_endpoint")] public Uri TokenEndpoint { get; private set; } [JsonProperty("capabilities")] @@ -54,5 +72,26 @@ public SmartConfigurationResult() [JsonProperty("scopes_supported")] public ICollection ScopesSupported { get; private set; } + + [JsonProperty("code_challenge_methods_supported")] + public ICollection CodeChallengeMethodsSupported { get; } + + [JsonProperty("grant_types_supported")] + public ICollection GrantTypesSupported { get; } + + [JsonProperty("token_endpoint_auth_methods_supported")] + public ICollection TokenEndpointAuthMethodsSupported { get; } + + [JsonProperty("response_types_supported")] + public ICollection ResponseTypesSupported { get; } + + [JsonProperty("introspection_endpoint")] + public string IntrospectionEndpoint { get; } + + [JsonProperty("management_endpoint")] + public string ManagementEndpoint { get; } + + [JsonProperty("revocation_endpoint")] + public string RevocationEndpoint { get; } } } diff --git a/src/Microsoft.Health.Fhir.Core/Messages/Get/GetSmartConfigurationResponse.cs b/src/Microsoft.Health.Fhir.Core/Messages/Get/GetSmartConfigurationResponse.cs index 7debdc63a0..1a5c0b8b61 100644 --- a/src/Microsoft.Health.Fhir.Core/Messages/Get/GetSmartConfigurationResponse.cs +++ b/src/Microsoft.Health.Fhir.Core/Messages/Get/GetSmartConfigurationResponse.cs @@ -22,7 +22,18 @@ public GetSmartConfigurationResponse(Uri authorizationEndpoint, Uri tokenEndpoin Capabilities = capabilities; } - public GetSmartConfigurationResponse(Uri authorizationEndpoint, Uri tokenEndpoint, ICollection capabilities, ICollection scopesSupported) + public GetSmartConfigurationResponse( + Uri authorizationEndpoint, + Uri tokenEndpoint, + ICollection capabilities, + ICollection scopesSupported, + ICollection codeChallengeMethodsSupported = null, + ICollection grantTypesSupported = null, + ICollection tokenEndpointAuthMethodsSupported = null, + ICollection responseTypesSupported = null, + string introspectionEndpoint = null, + string managementEndpoint = null, + string revocationEndpoint = null) { EnsureArg.IsNotNull(authorizationEndpoint, nameof(authorizationEndpoint)); EnsureArg.IsNotNull(tokenEndpoint, nameof(tokenEndpoint)); @@ -32,6 +43,13 @@ public GetSmartConfigurationResponse(Uri authorizationEndpoint, Uri tokenEndpoin TokenEndpoint = tokenEndpoint; Capabilities = capabilities; ScopesSupported = scopesSupported; + CodeChallengeMethodsSupported = codeChallengeMethodsSupported; + GrantTypesSupported = grantTypesSupported; + TokenEndpointAuthMethodsSupported = tokenEndpointAuthMethodsSupported; + ResponseTypesSupported = responseTypesSupported; + IntrospectionEndpoint = introspectionEndpoint; + ManagementEndpoint = managementEndpoint; + RevocationEndpoint = revocationEndpoint; } public Uri AuthorizationEndpoint { get; } @@ -41,5 +59,19 @@ public GetSmartConfigurationResponse(Uri authorizationEndpoint, Uri tokenEndpoin public ICollection Capabilities { get; } public ICollection ScopesSupported { get; } + + public ICollection CodeChallengeMethodsSupported { get; } + + public ICollection GrantTypesSupported { get; } + + public ICollection TokenEndpointAuthMethodsSupported { get; } + + public ICollection ResponseTypesSupported { get; } + + public string IntrospectionEndpoint { get; } + + public string ManagementEndpoint { get; } + + public string RevocationEndpoint { get; } } } 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 fb2c257a50..d3818fadc5 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 @@ -67,6 +67,10 @@ public async Task GivenASmartConfigurationHandler_WhenSecurityConfigurationEnabl // Verify SMART v2 scopes are included Assert.NotNull(response.ScopesSupported); + Assert.NotNull(response.CodeChallengeMethodsSupported); + Assert.NotNull(response.GrantTypesSupported); + Assert.NotNull(response.TokenEndpointAuthMethodsSupported); + Assert.NotNull(response.ResponseTypesSupported); } [Fact] @@ -84,5 +88,38 @@ public async Task GivenASmartConfigurationHandler_WhenBaseEndpointIsInvalid_Then OperationFailedException exception = await Assert.ThrowsAsync(() => handler.Handle(request, CancellationToken.None)); Assert.Equal(HttpStatusCode.BadRequest, exception.ResponseStatusCode); } + + [Theory] + [InlineData("https://ehr.example.com/user/introspect", null, null)] + [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( + string introspectionEndpoint, + string managementEndpoint, + string revocationEndpoint) + { + var request = new GetSmartConfigurationRequest(); + string baseEndpoint = "http://base.endpoint"; + + 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)); + + GetSmartConfigurationResponse response = await handler.Handle(request, CancellationToken.None); + + Assert.Equal(response.AuthorizationEndpoint.ToString(), baseEndpoint + "/authorize"); + Assert.Equal(response.TokenEndpoint.ToString(), baseEndpoint + "/token"); + + // Verify SMART v2 endpoints + Assert.Equal(introspectionEndpoint, response.IntrospectionEndpoint); + Assert.Equal(managementEndpoint, response.ManagementEndpoint); + Assert.Equal(revocationEndpoint, response.RevocationEndpoint); + } } } diff --git a/src/Microsoft.Health.Fhir.Shared.Core/Extensions/FhirMediatorExtensions.cs b/src/Microsoft.Health.Fhir.Shared.Core/Extensions/FhirMediatorExtensions.cs index 3e52301f8d..c6c0af2f6e 100644 --- a/src/Microsoft.Health.Fhir.Shared.Core/Extensions/FhirMediatorExtensions.cs +++ b/src/Microsoft.Health.Fhir.Shared.Core/Extensions/FhirMediatorExtensions.cs @@ -173,7 +173,18 @@ public static async Task GetSmartConfigurationAsync(th var response = await mediator.Send(new GetSmartConfigurationRequest(), cancellationToken); - return new SmartConfigurationResult(response.AuthorizationEndpoint, response.TokenEndpoint, response.Capabilities, response.ScopesSupported); + return new SmartConfigurationResult( + response.AuthorizationEndpoint, + response.TokenEndpoint, + response.Capabilities, + response.ScopesSupported, + response.CodeChallengeMethodsSupported, + response.GrantTypesSupported, + response.TokenEndpointAuthMethodsSupported, + response.ResponseTypesSupported, + response.IntrospectionEndpoint, + response.ManagementEndpoint, + response.RevocationEndpoint); } public static async Task GetOperationVersionsAsync(this IMediator mediator, CancellationToken cancellationToken = default)