-
Notifications
You must be signed in to change notification settings - Fork 539
Update smart-configuration and metadata for SMART on FHIR #3697
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
079b530
62f03b5
e815301
056710c
2a97d30
88ca518
2b8e616
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 SmartAuthenticationConfiguration | ||
{ | ||
public string Authority { get; set; } | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
// ------------------------------------------------------------------------------------------------- | ||
// Copyright (c) Microsoft Corporation. All rights reserved. | ||
// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. | ||
// ------------------------------------------------------------------------------------------------- | ||
|
||
using System; | ||
using Newtonsoft.Json; | ||
|
||
namespace Microsoft.Health.Fhir.Core.Features.Conformance.Models | ||
{ | ||
/// <summary> | ||
/// This class represents the minimum required properties of the OpenID Connect discovery document used in the capability statement. | ||
/// </summary> | ||
public class OpenIdConfigurationResponse | ||
{ | ||
public OpenIdConfigurationResponse( | ||
Uri authorizationEndpoint, | ||
Uri tokenEndpoint) | ||
{ | ||
AuthorizationEndpoint = authorizationEndpoint; | ||
TokenEndpoint = tokenEndpoint; | ||
} | ||
|
||
/// <summary> | ||
/// REQUIRED, URL to the OAuth2 authorization endpoint. | ||
/// </summary> | ||
[JsonProperty("authorization_endpoint", DefaultValueHandling = DefaultValueHandling.Ignore)] | ||
public Uri AuthorizationEndpoint { get; } | ||
|
||
/// <summary> | ||
/// REQUIRED, URL to the OAuth2 token endpoint. | ||
/// </summary> | ||
[JsonProperty("token_endpoint", DefaultValueHandling = DefaultValueHandling.Ignore)] | ||
public Uri TokenEndpoint { get; } | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
// ------------------------------------------------------------------------------------------------- | ||
// Copyright (c) Microsoft Corporation. All rights reserved. | ||
// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. | ||
// ------------------------------------------------------------------------------------------------- | ||
|
||
using System.Threading; | ||
using System.Threading.Tasks; | ||
using Microsoft.Health.Fhir.Core.Features.Conformance.Models; | ||
using Microsoft.Health.Fhir.Core.Messages.Get; | ||
|
||
namespace Microsoft.Health.Fhir.Core.Features.Conformance.Providers | ||
{ | ||
public interface IWellKnownConfigurationProvider | ||
{ | ||
bool IsSmartConfigured(); | ||
|
||
Task<GetSmartConfigurationResponse> GetSmartConfigurationAsync(CancellationToken cancellationToken); | ||
|
||
Task<OpenIdConfigurationResponse> GetOpenIdConfigurationAsync(CancellationToken cancellationToken); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
// ------------------------------------------------------------------------------------------------- | ||
// Copyright (c) Microsoft Corporation. All rights reserved. | ||
// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. | ||
// ------------------------------------------------------------------------------------------------- | ||
|
||
using System; | ||
using System.Net.Http; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
using EnsureThat; | ||
using Microsoft.Extensions.Logging; | ||
using Microsoft.Extensions.Options; | ||
using Microsoft.Health.Fhir.Core.Configs; | ||
using Microsoft.Health.Fhir.Core.Features.Conformance.Models; | ||
using Microsoft.Health.Fhir.Core.Messages.Get; | ||
using Newtonsoft.Json; | ||
|
||
namespace Microsoft.Health.Fhir.Core.Features.Conformance.Providers | ||
{ | ||
public class WellKnownConfigurationProvider : IWellKnownConfigurationProvider | ||
mikaelweave marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
private const string OpenIdConfigurationPath = ".well-known/openid-configuration"; | ||
private const string SmartConfigurationPath = ".well-known/smart-configuration"; | ||
|
||
private readonly SecurityConfiguration _securityConfiguration; | ||
private readonly IHttpClientFactory _httpClientFactory; | ||
private readonly ILogger<WellKnownConfigurationProvider> _logger; | ||
|
||
public WellKnownConfigurationProvider( | ||
IOptions<SecurityConfiguration> securityConfigurationOptions, | ||
IHttpClientFactory httpClientFactory, | ||
ILogger<WellKnownConfigurationProvider> logger) | ||
{ | ||
_securityConfiguration = EnsureArg.IsNotNull(securityConfigurationOptions?.Value, nameof(securityConfigurationOptions)); | ||
_httpClientFactory = EnsureArg.IsNotNull(httpClientFactory, nameof(httpClientFactory)); | ||
_logger = EnsureArg.IsNotNull(logger, nameof(logger)); | ||
} | ||
|
||
public bool IsSmartConfigured() | ||
{ | ||
return !string.IsNullOrWhiteSpace(_securityConfiguration?.SmartAuthentication?.Authority); | ||
} | ||
|
||
public Task<OpenIdConfigurationResponse> GetOpenIdConfigurationAsync(CancellationToken cancellationToken) | ||
{ | ||
return GetConfigurationAsync<OpenIdConfigurationResponse>(OpenIdConfigurationPath, cancellationToken); | ||
} | ||
|
||
public Task<GetSmartConfigurationResponse> GetSmartConfigurationAsync(CancellationToken cancellationToken) | ||
{ | ||
Uri configurationUrl = GetConfigurationUrl(SmartConfigurationPath); | ||
|
||
if (configurationUrl != null && string.Equals("localhost", configurationUrl.Host, StringComparison.OrdinalIgnoreCase)) | ||
{ | ||
_logger.LogInformation("The FHIR service is running locally with no Authority set, {URL} is not available.", SmartConfigurationPath); | ||
return Task.FromResult<GetSmartConfigurationResponse>(null); | ||
} | ||
|
||
return GetConfigurationAsync<GetSmartConfigurationResponse>(SmartConfigurationPath, cancellationToken); | ||
} | ||
|
||
private async Task<TConfiguration> GetConfigurationAsync<TConfiguration>(string configurationPath, CancellationToken cancellationToken) | ||
where TConfiguration : class | ||
{ | ||
Uri configurationUrl = GetConfigurationUrl(configurationPath); | ||
|
||
if (configurationUrl != null) | ||
{ | ||
_logger.LogInformation("Fetching configuration using well-known endpoint: {ConfigurationUrl}.", configurationUrl.AbsoluteUri); | ||
|
||
using HttpClient client = _httpClientFactory.CreateClient(); | ||
using var smartConfigurationRequest = new HttpRequestMessage(HttpMethod.Get, configurationUrl); | ||
HttpResponseMessage response = await client.SendAsync(smartConfigurationRequest, cancellationToken); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you leave Auth/Audience blank in appsettings, and simply run the OSS server, and then hit the .well-known/smart-configuration endpoint, you get an endless loop as the server makes a request against it's own endpoint :). I also wonder if we can't rely on an Oauth compliant IDP and then provide the "smart" compliance ourselves? I am concerned that there may be few full "smart" compliant IDPs and we may limit the usability of this if we can't augment Oauth to provide a well-known/smart-configuration endpoint from the FHIR service, which in turn relies on the .well-known/openid-configuration of the IDP. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you leave Auth/Audience blank in appsettings, and simply run the OSS server, and then hit the .well-known/smart-configuration endpoint, you get an endless loop as the server makes a request against it's own endpoint :). I guess I'm not sure why this would happen. If you leave Authority blank, does the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If Security is true, but no Auth or Audience is specified we use an in memory Identity Provider which loads users, roles and permitted data actions from role.json. This is for testing purposes. It will publish an authorize and token endpoint. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I also wonder if we can't rely on an Oauth compliant IDP and then provide the "smart" compliance ourselves? I am concerned that there may be few full "smart" compliant IDPs and we may limit the usability of this if we can't augment Oauth to provide a well-known/smart-configuration endpoint from the FHIR service, which in turn relies on the .well-known/openid-configuration of the IDP. I have a solution for this and we can talk about where this solution belongs (either this repository or a separate one). The main issue with SMART is that the response from .well-known/smart-configuration contains capabilities representing both the FHIR server and the IDP. The capabilities of the FHIR server and the IDP need to be combined by the implementor. This probably means that there are settings that need to be configured depending on the IDP. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I added a condition to address this specific case. Let me know what you think. |
||
|
||
if (response.IsSuccessStatusCode) | ||
{ | ||
_logger.LogInformation("Successfully fetched the configuration from {ConfigurationUrl}.", configurationUrl.AbsoluteUri); | ||
|
||
try | ||
{ | ||
string smartConfigurationJson = await response.Content.ReadAsStringAsync(cancellationToken: cancellationToken); | ||
return JsonConvert.DeserializeObject<TConfiguration>(smartConfigurationJson); | ||
} | ||
catch (JsonSerializationException ex) | ||
{ | ||
_logger.LogError(ex, "An error parsing the configuration response from \"{ConfigurationUrl}\" occurred.", configurationUrl.AbsoluteUri); | ||
return null; | ||
} | ||
} | ||
|
||
_logger.LogWarning("The configuration request to \"{ConfigurationUrl}\" returned a {StatusCode} status code.", configurationUrl.AbsoluteUri, response.StatusCode); | ||
return null; | ||
} | ||
|
||
_logger.LogInformation("Authority is not valid cannot process {ConfigurationPath} request.", configurationPath); | ||
|
||
return null; | ||
} | ||
|
||
private Uri GetConfigurationUrl(string configurationPath) | ||
{ | ||
// Prefer the SmartAuthentication authority, but default to Authentication authority. | ||
string authority = _securityConfiguration?.SmartAuthentication?.Authority ?? _securityConfiguration?.Authentication?.Authority; | ||
|
||
if (!string.IsNullOrWhiteSpace(authority)) | ||
{ | ||
if (!authority.EndsWith('/')) | ||
{ | ||
authority += "/"; | ||
} | ||
|
||
try | ||
{ | ||
return new Uri($"{authority}{configurationPath}"); | ||
} | ||
catch (UriFormatException) | ||
{ | ||
return null; | ||
} | ||
} | ||
|
||
return null; | ||
} | ||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.