diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 677ed3f7..93ec8eb6 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "dotnet-ef": { - "version": "8.0.4", + "version": "8.0.5", "commands": [ "dotnet-ef" ] diff --git a/Directory.Packages.props b/Directory.Packages.props index fe03489d..db7f255d 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -5,18 +5,18 @@ - + - - - - + + + + - - + + @@ -27,28 +27,28 @@ - + - + - - - - - - - + + + + + + + - + diff --git a/Udap.Metadata.Server/Configuration/DependencyInjection/ServiceCollectionExtensions.cs b/Udap.Metadata.Server/Configuration/DependencyInjection/ServiceCollectionExtensions.cs index 35c00429..37b8968f 100644 --- a/Udap.Metadata.Server/Configuration/DependencyInjection/ServiceCollectionExtensions.cs +++ b/Udap.Metadata.Server/Configuration/DependencyInjection/ServiceCollectionExtensions.cs @@ -21,7 +21,7 @@ using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using System.Net.Http; +using Hl7.Fhir.Utility; using Udap.Common; using Udap.Common.Certificates; using Udap.Common.Extensions; @@ -126,7 +126,7 @@ public static IApplicationBuilder UseUdapMetadataServer(this WebApplication app, { EnsureMvcControllerUnloads(app); - app.MapGet($"/{prefixRoute}{UdapConstants.Discovery.DiscoveryEndpoint}", + app.MapGet($"/{prefixRoute?.EnsureTrailingSlash().RemovePrefix("/")}{UdapConstants.Discovery.DiscoveryEndpoint}", async ( [FromServices] UdapMetaDataEndpoint endpoint, HttpContext httpContext, @@ -136,13 +136,13 @@ public static IApplicationBuilder UseUdapMetadataServer(this WebApplication app, .Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status404NotFound); // community doesn't exist - app.MapGet($"/{prefixRoute}{UdapConstants.Discovery.DiscoveryEndpoint}/communities", + app.MapGet($"/{prefixRoute?.EnsureTrailingSlash().RemovePrefix("/")}{UdapConstants.Discovery.DiscoveryEndpoint}/communities", ([FromServices] UdapMetaDataEndpoint endpoint) => endpoint.GetCommunities()) .AllowAnonymous() .Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status404NotFound); // community doesn't exist - app.MapGet($"/{prefixRoute}{UdapConstants.Discovery.DiscoveryEndpoint}/communities/ashtml", + app.MapGet($"/{prefixRoute?.EnsureTrailingSlash().RemovePrefix("/")}{UdapConstants.Discovery.DiscoveryEndpoint}/communities/ashtml", ( [FromServices] UdapMetaDataEndpoint endpoint, HttpContext httpContext) => endpoint.GetCommunitiesAsHtml(httpContext)) @@ -156,7 +156,7 @@ public static IApplicationBuilder UseUdapMetadataServer(this WebApplication app, public static IApplicationBuilder UseUdapMetadataServer(this IApplicationBuilder app, string? prefixRoute = null) { - app.Map($"/{prefixRoute}{UdapConstants.Discovery.DiscoveryEndpoint}", path => + app.Map($"/{prefixRoute?.EnsureTrailingSlash().RemovePrefix("/")}{UdapConstants.Discovery.DiscoveryEndpoint}", path => { path.Run(async ctx => { diff --git a/Udap.Metadata.Server/UdapMetaDataEndpoint.cs b/Udap.Metadata.Server/UdapMetaDataEndpoint.cs index 8f5bd940..066aa3aa 100644 --- a/Udap.Metadata.Server/UdapMetaDataEndpoint.cs +++ b/Udap.Metadata.Server/UdapMetaDataEndpoint.cs @@ -42,7 +42,6 @@ public IResult GetCommunities() return Results.Ok(_metaDataBuilder.GetCommunities()); } - public IResult GetCommunitiesAsHtml(HttpContext httpContext) { var html = _metaDataBuilder.GetCommunitiesAsHtml(httpContext.Request.GetDisplayUrl().GetBaseUrlFromMetadataUrl()); diff --git a/Udap.Model/Access/AccessTokenRequestForAuthorizationCodeBuilder.cs b/Udap.Model/Access/AccessTokenRequestForAuthorizationCodeBuilder.cs index f3e9a2d3..3ed62afa 100644 --- a/Udap.Model/Access/AccessTokenRequestForAuthorizationCodeBuilder.cs +++ b/Udap.Model/Access/AccessTokenRequestForAuthorizationCodeBuilder.cs @@ -67,21 +67,13 @@ public AccessTokenRequestForAuthorizationCodeBuilder WithClaim(Claim claim) } /// - /// Legacy refers to the current udap.org/UDAPTestTool behavior as documented in - /// udap.org profiles. The HL7 Security IG has the following constraint to make it - /// more friendly with OIDC and SMART launch frameworks. - /// sub == iss == client_id - /// Where as the Legacy is the following behavior - /// sub == iis == SubAlt Name + /// Build an /// - /// /// /// - public UdapAuthorizationCodeTokenRequest Build( - bool legacy = false, - string? algorithm = UdapConstants.SupportedAlgorithm.RS256) + public UdapAuthorizationCodeTokenRequest Build(string? algorithm = UdapConstants.SupportedAlgorithm.RS256) { - var clientAssertion = BuildClientAssertion(algorithm, legacy); + var clientAssertion = BuildClientAssertion(algorithm); return new UdapAuthorizationCodeTokenRequest() { @@ -99,34 +91,18 @@ public UdapAuthorizationCodeTokenRequest Build( }; } - private string? BuildClientAssertion(string algorithm, bool legacy = false) + private string? BuildClientAssertion(string algorithm) { JwtPayLoadExtension jwtPayload; - if (legacy) - { - //udap.org profile - jwtPayload = new JwtPayLoadExtension( - _certificate.GetNameInfo(X509NameType.UrlName, - false), //TODO:: Let user pick the subject alt name. Create will need extra param. - _tokenEndpoint, //The FHIR Authorization Server's token endpoint URL - _claims, - _now, - _now.AddMinutes(5) - ); - } - - else - { - //HL7 FHIR IG profile - jwtPayload = new JwtPayLoadExtension( - _clientId, - _tokenEndpoint, //The FHIR Authorization Server's token endpoint URL - _claims, - _now, - _now.AddMinutes(5) - ); - } + //HL7 FHIR IG profile + jwtPayload = new JwtPayLoadExtension( + _clientId, + _tokenEndpoint, //The FHIR Authorization Server's token endpoint URL + _claims, + _now, + _now.AddMinutes(5) + ); return SignedSoftwareStatementBuilder .Create(_certificate, jwtPayload) diff --git a/Udap.Model/Access/AccessTokenRequestForClientCredentialsBuilder.cs b/Udap.Model/Access/AccessTokenRequestForClientCredentialsBuilder.cs index 3df21b85..e9eb483d 100644 --- a/Udap.Model/Access/AccessTokenRequestForClientCredentialsBuilder.cs +++ b/Udap.Model/Access/AccessTokenRequestForClientCredentialsBuilder.cs @@ -91,21 +91,13 @@ public AccessTokenRequestForClientCredentialsBuilder WithExtension(string key, B } /// - /// Legacy refers to the current udap.org/UDAPTestTool behavior as documented in - /// udap.org profiles. The HL7 Security IG has the following constraint to make it - /// more friendly with OIDC and SMART launch frameworks. - /// sub == iss == client_id - /// Where as the Legacy is the following behavior - /// sub == iis == SubAlt Name + /// Build an /// - /// /// /// - public UdapClientCredentialsTokenRequest Build( - bool legacy = false, - string? algorithm = UdapConstants.SupportedAlgorithm.RS256) + public UdapClientCredentialsTokenRequest Build(string? algorithm = UdapConstants.SupportedAlgorithm.RS256) { - var clientAssertion = BuildClientAssertion(algorithm, legacy); + var clientAssertion = BuildClientAssertion(algorithm); return new UdapClientCredentialsTokenRequest { @@ -122,34 +114,18 @@ public UdapClientCredentialsTokenRequest Build( } - private string BuildClientAssertion(string algorithm, bool legacy = false) + private string BuildClientAssertion(string algorithm) { JwtPayLoadExtension jwtPayload; - - if (legacy) - { - //udap.org profile - jwtPayload = new JwtPayLoadExtension( - _certificate.GetNameInfo(X509NameType.UrlName, - false), //TODO:: Let user pick the subject alt name. Create will need extra param. - _tokenEndoint, //The FHIR Authorization Server's token endpoint URL - _claims, - _now, - _now.AddMinutes(5) - ); - } - - else - { - //HL7 FHIR IG profile - jwtPayload = new JwtPayLoadExtension( - _clientId, //TODO:: Let user pick the subject alt name. Create will need extra param. - _tokenEndoint, //The FHIR Authorization Server's token endpoint URL - _claims, - _now, - _now.AddMinutes(5) - ); - } + + //HL7 FHIR IG profile + jwtPayload = new JwtPayLoadExtension( + _clientId, //TODO:: Let user pick the subject alt name. Create will need extra param. + _tokenEndoint, //The FHIR Authorization Server's token endpoint URL + _claims, + _now, + _now.AddMinutes(5) + ); if (_extensions != null) { diff --git a/Udap.Model/Statement/SignedSoftwareStatementBuilder.cs b/Udap.Model/Statement/SignedSoftwareStatementBuilder.cs index f17a1c1c..9a15d4b8 100644 --- a/Udap.Model/Statement/SignedSoftwareStatementBuilder.cs +++ b/Udap.Model/Statement/SignedSoftwareStatementBuilder.cs @@ -40,19 +40,18 @@ public static SignedSoftwareStatementBuilder Create(X509Certificate2 certific // we could add more builder methods // - public string Build(string? algorithm = UdapConstants.SupportedAlgorithm.RS256) + public string Build(string? algorithm = null) { - algorithm ??= UdapConstants.SupportedAlgorithm.RS256; - #if NET5_0_OR_GREATER // // Short circuit to ECDSA // if (_certificate.GetECDsaPublicKey() != null) { - return BuildECDSA(); + return BuildECDSA(algorithm); } #endif + algorithm ??= UdapConstants.SupportedAlgorithm.RS256; var securityKey = new X509SecurityKey(_certificate); var signingCredentials = new SigningCredentials(securityKey, algorithm); @@ -75,11 +74,9 @@ public string Build(string? algorithm = UdapConstants.SupportedAlgorithm.RS256) #if NET5_0_OR_GREATER - public string BuildECDSA(string? algorithm = UdapConstants.SupportedAlgorithm.ES384) + public string BuildECDSA(string? algorithm = null) { - - algorithm ??= UdapConstants.SupportedAlgorithm.ES384; - + algorithm ??= UdapConstants.SupportedAlgorithm.ES256; var key = _certificate.GetECDsaPrivateKey(); using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP384); diff --git a/Udap.Server/Configuration/DependencyInjection/UdapBuilderExtensions/UdapCore.cs b/Udap.Server/Configuration/DependencyInjection/UdapBuilderExtensions/UdapCore.cs index 80673fed..7b980b0c 100644 --- a/Udap.Server/Configuration/DependencyInjection/UdapBuilderExtensions/UdapCore.cs +++ b/Udap.Server/Configuration/DependencyInjection/UdapBuilderExtensions/UdapCore.cs @@ -116,7 +116,7 @@ public static IUdapServiceBuilder AddUdapResponseGenerators(this IUdapServiceBui public static IUdapServiceBuilder AddPrivateFileStore(this IUdapServiceBuilder builder, string? resourceServerName = null) { - builder.Services.AddSingleton(sp => + builder.Services.TryAddSingleton(sp => new IssuedCertificateStore( sp.GetRequiredService>(), sp.GetRequiredService>(), diff --git a/Udap.Server/Configuration/DependencyInjection/UdapServerServiceCollectionExtensions.cs b/Udap.Server/Configuration/DependencyInjection/UdapServerServiceCollectionExtensions.cs index b04bce8c..5716bfce 100644 --- a/Udap.Server/Configuration/DependencyInjection/UdapServerServiceCollectionExtensions.cs +++ b/Udap.Server/Configuration/DependencyInjection/UdapServerServiceCollectionExtensions.cs @@ -28,6 +28,7 @@ using Udap.Server.Mappers; using Udap.Server.Options; using Udap.Server.ResponseHandling; +using Udap.Server.Security.Authentication.TieredOAuth; using Udap.Server.Stores; using Udap.Server.Validation; using static Udap.Server.Constants; @@ -166,6 +167,7 @@ public static IUdapServiceBuilder AddUdapServerAsIdentityProvider( services.Configure(setupAction); } + builder.Services.TryAddSingleton, TieredIdpServerSettings>(); builder.AddUdapSigningCredentials(); services.AddSingleton(resolver => resolver.GetRequiredService>().Value); builder.AddRegistrationEndpointToOpenIdConnectMetadata(baseUrl); diff --git a/Udap.Server/Configuration/ServerSettings.cs b/Udap.Server/Configuration/ServerSettings.cs index 822866e1..933ffaef 100644 --- a/Udap.Server/Configuration/ServerSettings.cs +++ b/Udap.Server/Configuration/ServerSettings.cs @@ -13,10 +13,6 @@ namespace Udap.Server.Configuration; public class ServerSettings { - [JsonPropertyName("ServerSupport")] - [JsonConverter(typeof(JsonStringEnumConverter))] - public ServerSupport ServerSupport { get; set; } - [JsonPropertyName("DefaultSystemScopes")] public string? DefaultSystemScopes { get; set; } @@ -34,7 +30,13 @@ public class ServerSettings /// [JsonPropertyName("ForceStateParamOnAuthorizationCode")] public bool ForceStateParamOnAuthorizationCode { get; set; } = false; - + + /// + /// Indicate if the IdentityServer can act as a UDAP enabled IdP. + /// + [JsonIgnore] + public bool TieredIdp { get; set; } = false; + [JsonPropertyName("LogoRequired")] public bool LogoRequired { get; set; } = true; @@ -50,12 +52,6 @@ public class ServerSettings } -public enum ServerSupport -{ - UDAP = 0, - Hl7SecurityIG = 1 -} - public static class ConfigurationExtension { public static TOptions GetOption(this IConfiguration configuration, string settingKey) diff --git a/Udap.Server/Extensions/ClientExtensions.cs b/Udap.Server/Extensions/ClientExtensions.cs index b48ec54f..5fbd9166 100644 --- a/Udap.Server/Extensions/ClientExtensions.cs +++ b/Udap.Server/Extensions/ClientExtensions.cs @@ -63,10 +63,13 @@ public static class ClientExtensions return null; } - public static Task> GetUdapKeysAsync(this ParsedSecret secret) + public static IEnumerable? GetUdapKeys(this ParsedSecret secret) { var jsonWebToken = new JsonWebToken(secret.Credential as string); - var x5cArray = jsonWebToken.GetHeaderValue>("x5c"); + if (!jsonWebToken.TryGetHeaderValue>("x5c", out var x5cArray)) + { + return null; + } var certificates = x5cArray .Select(s => new X509Certificate2(Convert.FromBase64String(s.ToString()))) @@ -81,13 +84,17 @@ public static Task> GetUdapKeysAsync(this ParsedSecret secret) }) .ToList(); - return Task.FromResult(certificates); + return certificates; } - public static X509Certificate2 GetUdapEndCertAsync(this ParsedSecret secret) + public static X509Certificate2? GetUdapEndCertAsync(this ParsedSecret secret) { var jsonWebToken = new JsonWebToken(secret.Credential as string); - var x5cArray = jsonWebToken.GetHeaderValue>("x5c"); + + if(!jsonWebToken.TryGetHeaderValue>("x5c", out var x5cArray)) + { + return null; + } return new X509Certificate2(Convert.FromBase64String(x5cArray.First())); } diff --git a/Udap.Server/Hosting/UdapAuthorizationResponseMiddleware.cs b/Udap.Server/Hosting/UdapAuthorizationResponseMiddleware.cs index 102c83a4..c269ed64 100644 --- a/Udap.Server/Hosting/UdapAuthorizationResponseMiddleware.cs +++ b/Udap.Server/Hosting/UdapAuthorizationResponseMiddleware.cs @@ -18,6 +18,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Primitives; using Udap.Server.Configuration; +using Udap.Util.Extensions; using static IdentityModel.OidcConstants; namespace Udap.Server.Hosting; @@ -81,7 +82,7 @@ public async Task Invoke( context.Request.Path.Value.Contains(Constants.ProtocolRoutePaths.Authorize)) { var requestParams = context.Request.Query; - + if (requestParams.Any()) { if (udapServerOptions.ForceStateParamOnAuthorizationCode) @@ -90,7 +91,7 @@ public async Task Invoke( { var client = await clients.FindClientByIdAsync( - requestParams.AsNameValueCollection().Get(AuthorizeRequest.ClientId)); + requestParams.AsNameValueCollection().Get(AuthorizeRequest.ClientId) ?? string.Empty); if (client != null && client.ClientSecrets.Any(cs => @@ -98,6 +99,37 @@ await clients.FindClientByIdAsync( { await RenderMissingStateErrorResponse(context); _logger.LogInformation($"{nameof(UdapAuthorizationResponseMiddleware)} executed"); + + return; + } + } + } + + // + // During Tiered OAuth from data holder to IdP the udap and idp scopes are required + // https://hl7.org/fhir/us/udap-security/user.html#data-holder-authentication-request-to-idp + // + if (udapServerOptions.TieredIdp) + { + var requestParamCollection = context.Request.Query.AsNameValueCollection(); + var client = + await clients.FindClientByIdAsync( + requestParamCollection.Get(AuthorizeRequest.ClientId) ?? string.Empty); + var scope = requestParamCollection.Get(AuthorizeRequest.Scope); + + var scopes = scope?.FromSpaceSeparatedString(); + var udap = (scopes ?? new string[] { }).FirstOrDefault(s => s == "udap"); + var openid = (scopes ?? new string[] { }).FirstOrDefault(s => s == "openid"); + + if (client != null && + client.ClientSecrets.Any(cs => + cs.Type == UdapServerConstants.SecretTypes.UDAP_SAN_URI_ISS_NAME)) + { + if (udap.IsNullOrEmpty() || openid.IsNullOrEmpty()) + { + await RenderRequiredScopeErrorResponse(context); + _logger.LogInformation($"{nameof(UdapAuthorizationResponseMiddleware)} executed"); + return; } } @@ -121,7 +153,7 @@ await clients.FindClientByIdAsync( var client = await clients.FindClientByIdAsync( requestParamCollection.Get(AuthorizeRequest.ClientId)); - var scope = requestParamCollection.Get(AuthorizeRequest.Scope); + if (client == null) { @@ -144,6 +176,25 @@ await clients.FindClientByIdAsync( await _next(context); } + private Task RenderRequiredScopeErrorResponse(HttpContext context) + { + if (context.Request.Query.TryGetValue( + AuthorizeRequest.RedirectUri, + out StringValues redirectUri)) + { + var url = BuildRedirectUrl( + context, + redirectUri, + AuthorizeErrors.InvalidRequest, + "Missing udap and/or openid scope between data holder and IdP"); + + context.Response.Redirect(url); + } + + return Task.CompletedTask; + } + + private Task RenderMissingStateErrorResponse(HttpContext context) { if (context.Request.Query.TryGetValue( diff --git a/Udap.Server/Hosting/UdapScopeEnrichmentMiddleware.cs b/Udap.Server/Hosting/UdapScopeEnrichmentMiddleware.cs index 926ca09c..e0524ce1 100644 --- a/Udap.Server/Hosting/UdapScopeEnrichmentMiddleware.cs +++ b/Udap.Server/Hosting/UdapScopeEnrichmentMiddleware.cs @@ -43,7 +43,7 @@ public async Task Invoke( IClientStore clients, IIdentityServerInteractionService interactionService) { - if (_udapServerOptions.ServerSupport == ServerSupport.UDAP && + if ( context.Request.Path.Value != null && context.Request.Path.Value.Contains(Constants.ProtocolRoutePaths.Token)) { diff --git a/Udap.Server/Models/UdapIdentityResources.cs b/Udap.Server/Models/UdapIdentityResources.cs index 1e6e86b1..10c4163c 100644 --- a/Udap.Server/Models/UdapIdentityResources.cs +++ b/Udap.Server/Models/UdapIdentityResources.cs @@ -56,8 +56,11 @@ public Profile() UserClaims.Add(UdapConstants.JwtClaimTypes.Hl7Identifier); } } +} - public class Udap : IdentityResource +public static class UdapApiScopes +{ + public class Udap : ApiScope { /// /// Initializes a new instance of the class. diff --git a/Udap.Server/Registration/UdapDynamicClientRegistrationValidator.cs b/Udap.Server/Registration/UdapDynamicClientRegistrationValidator.cs index fe118aed..3df8586d 100644 --- a/Udap.Server/Registration/UdapDynamicClientRegistrationValidator.cs +++ b/Udap.Server/Registration/UdapDynamicClientRegistrationValidator.cs @@ -88,10 +88,7 @@ IEnumerable anchors ) { using var activity = Tracing.ValidationActivitySource.StartActivity("UdapDynamicClientRegistrationValidator.Validate"); - - _logger.LogDebug($"Start client validation with Server Support Type {_serverSettings.ServerSupport}"); - - + var tokenHandler = new JsonWebTokenHandler(); var jsonWebToken = tokenHandler.ReadJsonWebToken(request.SoftwareStatement); var jwtHeader = JwtHeader.Base64UrlDeserialize(jsonWebToken.EncodedHeader); @@ -452,52 +449,13 @@ IEnumerable anchors ////////////////////////////// if (client.AllowedGrantTypes.Count != 0 && //Cancel Registration - _serverSettings.ServerSupport == ServerSupport.Hl7SecurityIG && (document.Scope == null || !document.Scope.Any())) { return await Task.FromResult(new UdapDynamicClientRegistrationValidationResult( UdapDynamicClientRegistrationErrors.InvalidClientMetadata, "scope is required")); } - - // Enrich Scopes: Todo: inject a ScopeEnricher - // TODO: Need a policy engine for various things. UDAP ServerMode allows and empty scope during registration. - // So some kind of policy linked to maybe issued certificate certification and/or community or something - // There are a lot of choices left up to a community. The HL7 ServerMode requires scopes to be sent during registration. - // This doesn't mean the problem is easier it just means we could filter down during registration even if policy - // allowed for a broader list of scopes. - // Below I use ServerSettings from appsettings. This basically says that server is either UDAP or HL7 mode. Well - // sort of. The code is only trying to pass udap.org tests and survive a HL7 connect-a-thon. By putting the logic in - // a policy engine we can have one server UDAP and Hl7 Mode or whatever the policy engine allows. - - // - // Also there should be a better way to do this. It will repeat many scope entries per client. - // - if (_serverSettings.ServerSupport == ServerSupport.UDAP) - { - if (string.IsNullOrWhiteSpace(document.Scope)) - { - IEnumerable? scopes = null; - - if (document.GrantTypes != null && document.GrantTypes.Contains(OidcConstants.GrantTypes.ClientCredentials)) - { - scopes = _serverSettings.DefaultSystemScopes?.FromSpaceSeparatedString(); - } - else if (document.GrantTypes != null && document.GrantTypes.Contains(OidcConstants.GrantTypes.AuthorizationCode)) - { - scopes = _serverSettings.DefaultUserScopes?.FromSpaceSeparatedString(); - } - - if (scopes != null) - { - foreach (var scope in scopes) - { - client?.AllowedScopes.Add(scope); - } - } - } - } if (document.Scope != null && document.Any()) { var scopes = document.Scope.Split(' ', StringSplitOptions.RemoveEmptyEntries); diff --git a/Udap.Server/Security/Authentication/TieredOAuth/TieredIdpServerSettings.cs b/Udap.Server/Security/Authentication/TieredOAuth/TieredIdpServerSettings.cs new file mode 100644 index 00000000..05c2c843 --- /dev/null +++ b/Udap.Server/Security/Authentication/TieredOAuth/TieredIdpServerSettings.cs @@ -0,0 +1,26 @@ +#region (c) 2024 Joseph Shook. All rights reserved. +// /* +// Authors: +// Joseph Shook Joseph.Shook@Surescripts.com +// +// See LICENSE in the project root for license information. +// */ +#endregion + +using Microsoft.Extensions.Options; +using Udap.Server.Configuration; + +namespace Udap.Server.Security.Authentication.TieredOAuth; + +public class TieredIdpServerSettings : IPostConfigureOptions +{ + /// + /// Invoked to configure a instance. + /// + /// The name of the options instance being configured. + /// The options instance to configured. + public void PostConfigure(string? name, ServerSettings options) + { + options.TieredIdp = true; + } +} \ No newline at end of file diff --git a/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthAuthenticationHandler.cs b/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthAuthenticationHandler.cs index ad7c8e54..3727f963 100644 --- a/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthAuthenticationHandler.cs +++ b/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthAuthenticationHandler.cs @@ -456,6 +456,14 @@ protected override async Task HandleChallengeAsync(AuthenticationProperties prop var idpParam = (requestParams.GetValues("idp") ?? throw new InvalidOperationException()).Last(); var scope = (requestParams.GetValues("scope") ?? throw new InvalidOperationException()).First(); + + var udap = scope.FromSpaceSeparatedString().FirstOrDefault(s => s == UdapConstants.StandardScopes.Udap); + + if (udap == null) + { + throw new Exception("Missing required udap scope from client for Tiered OAuth"); + } + var clientRedirectUrl = (requestParams.GetValues("redirect_uri") ?? throw new InvalidOperationException()).Last(); var updateRegistration = requestParams.GetValues("update_registration")?.Last(); diff --git a/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthAuthenticationOptions.cs b/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthAuthenticationOptions.cs index 1f53d9a2..14ad9554 100644 --- a/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthAuthenticationOptions.cs +++ b/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthAuthenticationOptions.cs @@ -11,6 +11,7 @@ using Microsoft.IdentityModel.Tokens; using System.IdentityModel.Tokens.Jwt; using IdentityModel; +using Udap.Model; namespace Udap.Server.Security.Authentication.TieredOAuth; @@ -24,6 +25,7 @@ public TieredOAuthAuthenticationOptions() SignInScheme = TieredOAuthAuthenticationDefaults.AuthenticationScheme; // TODO: configurable for the non-dynamic AddTieredOAuthForTests call. + Scope.Add(UdapConstants.StandardScopes.Udap); Scope.Add(OidcConstants.StandardScopes.OpenId); Scope.Add(OidcConstants.StandardScopes.Email); Scope.Add(OidcConstants.StandardScopes.Profile); diff --git a/Udap.Server/Validation/Default/UdapJwtSecretValidator.cs b/Udap.Server/Validation/Default/UdapJwtSecretValidator.cs index f2baf89a..7b309b42 100644 --- a/Udap.Server/Validation/Default/UdapJwtSecretValidator.cs +++ b/Udap.Server/Validation/Default/UdapJwtSecretValidator.cs @@ -19,6 +19,7 @@ using Microsoft.IdentityModel.Tokens; using Udap.Common.Certificates; using Udap.Common.Extensions; +using Udap.Model; using Udap.Server.Configuration; using Udap.Server.Extensions; using Udap.Server.Storage.Stores; @@ -64,7 +65,6 @@ public UdapJwtSecretValidator( } - //Todo: Write replay unit tests //Todo: Write workflow diagrams to describe this process /// Validates a secret @@ -107,11 +107,11 @@ public async Task ValidateAsync(IEnumerable secr if (certChainList != null && !certChainList.Any()) { - _logger.LogError("There are no anchors available to validate client assertion."); + _logger.LogError($"There are no anchors available to validate client assertion for cient_id: {parsedSecret.Id}"); return fail; } - + var validAudiences = new[] { // token endpoint URL @@ -122,11 +122,10 @@ public async Task ValidateAsync(IEnumerable secr }.Distinct(); var tokenHandler = new JsonWebTokenHandler() { MaximumTokenSizeInBytes = _options.InputLengthRestrictions.Jwt }; - var jsonWebToken = tokenHandler.ReadJsonWebToken(jwtTokenString); - + var tokenValidationParameters = new TokenValidationParameters { - IssuerSigningKeys = await parsedSecret.GetUdapKeysAsync(), + IssuerSigningKeys = parsedSecret.GetUdapKeys(), ValidateIssuerSigningKey = true, ValidIssuer = parsedSecret.Id, @@ -138,47 +137,43 @@ public async Task ValidateAsync(IEnumerable secr RequireSignedTokens = true, RequireExpirationTime = true, - ValidAlgorithms = new[] { jsonWebToken!.GetHeaderValue(JwtHeaderParameterNames.Alg) }, + ValidAlgorithms = new[] { + UdapConstants.SupportedAlgorithm.RS256, UdapConstants.SupportedAlgorithm.RS384, + UdapConstants.SupportedAlgorithm.ES256, UdapConstants.SupportedAlgorithm.ES384 }, ClockSkew = TimeSpan.FromMinutes(5), - // ValidateSignatureLast = true + ValidateSignatureLast = true }; - - - if (_serverSettings.ServerSupport == ServerSupport.UDAP) - { - - tokenValidationParameters.IssuerValidator = (issuer, token, parameters) => - { - if (issuer != null && jsonWebToken.Claims.FirstOrDefault(c => c.Issuer == issuer) != null) - { - return issuer; - } - - return null; - }; - } - var result = tokenHandler.ValidateToken(jwtTokenString, tokenValidationParameters); if (!result.IsValid) { _logger.LogError(result.Exception, "JWT token validation error"); - + + var jsonWebToken = tokenHandler.ReadJsonWebToken(jwtTokenString); + + if (!jsonWebToken!.TryGetHeaderValue(JwtHeaderParameterNames.Alg, out string _)) + { + _logger.LogError($"Missing jwt x5c claim in header for client_id: {parsedSecret.Id}"); + } + + if (!jsonWebToken!.TryGetHeaderValue(JwtHeaderParameterNames.X5c, out string _)) + { + _logger.LogError($"Missing jwt x5c claim in header for client_id: {parsedSecret.Id}"); + } + return fail; } var jwtToken = (JsonWebToken)result.SecurityToken; - if (_serverSettings.ServerSupport == ServerSupport.Hl7SecurityIG) + + if (jwtToken.Subject != jwtToken.Issuer) { - if (jwtToken.Subject != jwtToken.Issuer) - { - _logger.LogError("Both 'sub' and 'iss' in the client assertion token must have a value of client_id."); - return fail; - } + _logger.LogError("Both 'sub' and 'iss' in the client assertion token must have a value of client_id."); + return fail; } diff --git a/Udap.Server/docs/README.md b/Udap.Server/docs/README.md index db75c561..29a49163 100644 --- a/Udap.Server/docs/README.md +++ b/Udap.Server/docs/README.md @@ -39,7 +39,6 @@ builder.Services.AddIdentityServer() var udapServerOptions = builder.Configuration.GetOption("ServerSettings"); options.DefaultSystemScopes = udapServerOptions.DefaultSystemScopes; options.DefaultUserScopes = udapServerOptions.DefaultUserScopes; - options.ServerSupport = udapServerOptions.ServerSupport; options.ForceStateParamOnAuthorizationCode = udapServerOptions. ForceStateParamOnAuthorizationCode; }, diff --git a/Udap.UI/Pages/UdapTieredLogin/Challenge.cshtml b/Udap.UI/Pages/UdapTieredLogin/Challenge.cshtml index 885d4393..2e5efc7f 100644 --- a/Udap.UI/Pages/UdapTieredLogin/Challenge.cshtml +++ b/Udap.UI/Pages/UdapTieredLogin/Challenge.cshtml @@ -9,11 +9,12 @@ - + Challenge
- + Failed to process Tiered OAuth request. + Ensure the udap scope and idp parameter are included.
\ No newline at end of file diff --git a/Udap.UI/Pages/UdapTieredLogin/Challenge.cshtml.cs b/Udap.UI/Pages/UdapTieredLogin/Challenge.cshtml.cs index 6b23a2a6..cd26626c 100644 --- a/Udap.UI/Pages/UdapTieredLogin/Challenge.cshtml.cs +++ b/Udap.UI/Pages/UdapTieredLogin/Challenge.cshtml.cs @@ -11,6 +11,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.Logging; using Udap.Client.Client; using Udap.Server.Security.Authentication.TieredOAuth; @@ -22,25 +23,36 @@ public class Challenge : PageModel { private readonly IIdentityServerInteractionService _interactionService; private readonly IUdapClient _udapClient; + private readonly ILogger _logger; - public Challenge(IIdentityServerInteractionService interactionService, IUdapClient udapClient) + public Challenge(IIdentityServerInteractionService interactionService, IUdapClient udapClient, ILogger logger) { _interactionService = interactionService; _udapClient = udapClient; + _logger = logger; } public async Task OnGetAsync(string scheme, string returnUrl) { if (string.IsNullOrEmpty(returnUrl)) returnUrl = "~/"; - - var props = await TieredOAuthHelpers.BuildDynamicTieredOAuthOptions( - _interactionService, - _udapClient, - scheme, - "/udaptieredlogin/callback", - returnUrl); - // start challenge and roundtrip the return URL and scheme - return Challenge(props, scheme); + try + { + var props = await TieredOAuthHelpers.BuildDynamicTieredOAuthOptions( + _interactionService, + _udapClient, + scheme, + "/udaptieredlogin/callback", + returnUrl); + + // start challenge and roundtrip the return URL and scheme + return Challenge(props, scheme); + } + catch (Exception ex) + { + _logger.LogWarning($"Failed Tiered Oauth for returnUrl: {returnUrl}"); + } + + return Page(); } } \ No newline at end of file diff --git a/Udap.sln b/Udap.sln index ce24285b..8d955d04 100644 --- a/Udap.sln +++ b/Udap.sln @@ -21,6 +21,7 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{522A1681-215E-4C39-B1AC-1324F7CE516E}" ProjectSection(SolutionItems) = preProject .dockerignore = .dockerignore + .gitignore = .gitignore Common.props = Common.props Directory.Build.props = Directory.Build.props Directory.Packages.props = Directory.Packages.props @@ -82,9 +83,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Udap.UI", "Udap.UI\Udap.UI. EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Udap.Proxy.Server", "examples\Udap.Proxy.Server\Udap.Proxy.Server.csproj", "{BC032973-A216-483C-8C5B-C7B5D9EB0D19}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Udap.Smart.Metadata", "Udap.Smart.Metadata\Udap.Smart.Metadata.csproj", "{6E2FC3C1-53B0-46F6-981F-58AC96462F2F}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Udap.Smart.Metadata", "Udap.Smart.Metadata\Udap.Smart.Metadata.csproj", "{6E2FC3C1-53B0-46F6-981F-58AC96462F2F}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Udap.Smart.Model", "Udap.Smart.Model\Udap.Smart.Model.csproj", "{DD9B2367-11A6-448C-B733-F5F436A0AA87}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Udap.Smart.Model", "Udap.Smart.Model\Udap.Smart.Model.csproj", "{DD9B2367-11A6-448C-B733-F5F436A0AA87}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/_tests/Directory.Packages.props b/_tests/Directory.Packages.props index abdbeecf..a39619ab 100644 --- a/_tests/Directory.Packages.props +++ b/_tests/Directory.Packages.props @@ -6,31 +6,31 @@ - - + + - + - - - - + + + + - + - - + + - + \ No newline at end of file diff --git a/_tests/Udap.Client.System.Tests/IdServerRegistrationTests.cs b/_tests/Udap.Client.System.Tests/IdServerRegistrationTests.cs index f4652467..21e12f25 100644 --- a/_tests/Udap.Client.System.Tests/IdServerRegistrationTests.cs +++ b/_tests/Udap.Client.System.Tests/IdServerRegistrationTests.cs @@ -1187,9 +1187,10 @@ public async Task RegistrationSuccess_authorization_code_FhirLabs_desktop_Test() "mailto:Joseph.Shook@Surescripts.com", "mailto:JoeShook@gmail.com" }) .WithTokenEndpointAuthMethod(UdapConstants.RegistrationDocumentValues.TokenEndpointAuthMethodValue) - // .WithScope("user/Patient.* user/Practitioner.read") //Comment out for UDAP Server mode. + .WithScope("user/Patient.read") //Comment out for UDAP Server mode. .WithResponseTypes(new HashSet { "code" }) .WithRedirectUrls(new List { new Uri($"https://client.fhirlabs.net/redirect/{Guid.NewGuid()}").AbsoluteUri }!) + .WithLogoUri("https://avatars.githubusercontent.com/u/77421324?s=48&v=4") .Build(); @@ -1215,7 +1216,7 @@ public async Task RegistrationSuccess_authorization_code_FhirLabs_desktop_Test() using var idpClient = new HttpClient(); // New client. The existing HttpClient chains up to a CustomTrustStore var response = await idpClient.PostAsJsonAsync(reg, requestBody); - response.StatusCode.Should().Be(HttpStatusCode.Created); + response.StatusCode.Should().BeOneOf(HttpStatusCode.Created, HttpStatusCode.OK); response.Content.Headers.ContentType!.ToString().Should().Be("application/json"); // var documentAsJson = JsonSerializer.Serialize(document); @@ -1295,7 +1296,7 @@ public async Task RegistrationSuccess_authorization_code_FhirLabs_desktop_Test() clientId: result.ClientId!, responseType: "code", state: CryptoRandom.CreateUniqueId(), - scope: "udap user.cruds", + scope: "user/Patient.read", redirectUri: document.RedirectUris!.First()); _testOutputHelper.WriteLine(url); @@ -1308,7 +1309,7 @@ public async Task RegistrationSuccess_authorization_code_FhirLabs_desktop_Test() response.StatusCode.Should().Be(HttpStatusCode.Redirect); var authUri = new Uri(disco.AuthorizeEndpoint!); - var loginUrl = $"{authUri.Scheme}://{authUri.Authority}/Account/Login"; + var loginUrl = $"{authUri.Scheme}://{authUri.Authority}/udapaccount/login"; response.Headers.Location?.ToString().Should() .StartWith(loginUrl); @@ -1335,17 +1336,6 @@ public async Task RegistrationSuccess_authorization_code_FhirLabs_desktop_Test() } - - // - // IDP Server must be running in ServerSupport mode of ServerSupport.UDAP for this to fail and pass the test. - // See part of test where getting Access Token - // var jwtPayload = new JwtPayload( - // result.Issuer, - // - // vs normal - // - // var jwtPayload = new JwtPayload( - // result.ClientId, // // If you want Udap.Idp to run in UDAP mode the use "ASPNETCORE_ENVIRONMENT": "Production" to launch. Or // however you get the serer to pickup appsettings.Production.json diff --git a/_tests/Udap.PKI.Generator/BuildNginxProxySSLCerts.cs b/_tests/Udap.PKI.Generator/BuildNginxProxySSLCerts.cs index 9f9bc2a6..b526f834 100644 --- a/_tests/Udap.PKI.Generator/BuildNginxProxySSLCerts.cs +++ b/_tests/Udap.PKI.Generator/BuildNginxProxySSLCerts.cs @@ -176,8 +176,31 @@ public static IEnumerable SSLProxyCerts() }; } + + public static IEnumerable Hl7SRI() + { + yield return new object[] + { + "CN=identity-matching.fast.hl7.org", //DistinguishedName + "identity-matching.fast.hl7.org" //SubjAltName + }; + + yield return new object[] + { + "CN=national-directory.fast.hl7.org", //DistinguishedName + "national-directory.fast.hl7.org" //SubjAltName + }; + + yield return new object[] + { + "CN=udap-security.fast.hl7.org", //DistinguishedName + "udap-security.fast.hl7.org" //SubjAltName + }; + } + [Theory(Skip = "Enabled on desktop when needed.")] [MemberData(nameof(SSLProxyCerts))] + [MemberData(nameof(Hl7SRI))] public void MakeIdentityProviderCertificates(string dn, string san) { using var rootCA = new X509Certificate2($"{SureFhirLabsCertStore}/ngnix-proxy-TestCA.pfx", "udap-test"); @@ -193,6 +216,8 @@ public void MakeIdentityProviderCertificates(string dn, string san) } + + private X509Certificate2 BuildSslCertificate( X509Certificate2? caCert, string distinguishedName, diff --git a/_tests/UdapServer.Tests/Common/ConnectaThon/HealthGorillaTests.cs b/_tests/UdapServer.Tests/Common/ConnectaThon/HealthGorillaTests.cs index 5d0c8030..69398bbd 100644 --- a/_tests/UdapServer.Tests/Common/ConnectaThon/HealthGorillaTests.cs +++ b/_tests/UdapServer.Tests/Common/ConnectaThon/HealthGorillaTests.cs @@ -8,8 +8,10 @@ using Duende.IdentityServer.Models; using Duende.IdentityServer.Test; using FluentAssertions; +using FluentAssertions.Common; using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; using Udap.Client.Configuration; using Udap.Common.Models; using Udap.Model; @@ -37,17 +39,11 @@ public HealthGorillaTests(ITestOutputHelper testOutputHelper) var sureFhirLabsAnchor = new X509Certificate2("CertStore/anchors/SureFhirLabs_CA.cer"); var intermediateCert = new X509Certificate2("CertStore/intermediates/SureFhirLabs_Intermediate.cer"); - _mockPipeline.OnPostConfigureServices += s => + _mockPipeline.OnPostConfigureServices += services => { - s.AddSingleton(new ServerSettings - { - ServerSupport = ServerSupport.UDAP, - DefaultUserScopes = "udap", - DefaultSystemScopes = "udap", - ForceStateParamOnAuthorizationCode = true - }); - - s.AddSingleton(new UdapClientOptions + services.AddSingleton(sp => sp.GetRequiredService>().Value); + + services.AddSingleton(new UdapClientOptions { ClientName = "Mock Client", Contacts = new HashSet { "mailto:Joseph.Shook@Surescripts.com", "mailto:JoeShook@gmail.com" } diff --git a/_tests/UdapServer.Tests/Conformance/Basic/ClientCredentialsUdapModeTests.cs b/_tests/UdapServer.Tests/Conformance/Basic/ClientCredentialsUdapModeTests.cs index 8d617614..fb1e2766 100644 --- a/_tests/UdapServer.Tests/Conformance/Basic/ClientCredentialsUdapModeTests.cs +++ b/_tests/UdapServer.Tests/Conformance/Basic/ClientCredentialsUdapModeTests.cs @@ -7,6 +7,8 @@ // */ #endregion + +using System.IdentityModel.Tokens.Jwt; using System.Net; using System.Net.Http.Headers; using System.Net.Http.Json; @@ -20,7 +22,9 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.JsonWebTokens; using Microsoft.IdentityModel.Tokens; +using Newtonsoft.Json.Linq; using NSubstitute; using Udap.Client.Client; using Udap.Client.Client.Extensions; @@ -35,6 +39,7 @@ using Udap.Util.Extensions; using UdapServer.Tests.Common; using Xunit.Abstractions; +using JwtHeaderParameterNames = Microsoft.IdentityModel.JsonWebTokens.JwtHeaderParameterNames; namespace UdapServer.Tests.Conformance.Basic; @@ -58,7 +63,6 @@ public ClientCredentialsUdapModeTests(ITestOutputHelper testOutputHelper) { services.AddSingleton(new ServerSettings { - ServerSupport = ServerSupport.UDAP, DefaultUserScopes = "udap", DefaultSystemScopes = "udap" }); @@ -237,6 +241,303 @@ public async Task GetAccessToken() } + [Fact] + public async Task GetAccessToken_Without_algorithm() + { + var clientCert = new X509Certificate2("CertStore/issued/fhirlabs.net.client.pfx", "udap-test"); + + var udapClient = _mockPipeline.Resolve(); + + // + // Typically the client would validate a server before proceeding to registration. + // + udapClient.UdapServerMetaData = new UdapMetadata(Substitute.For(), Substitute.For>()) + { RegistrationEndpoint = UdapAuthServerPipeline.RegistrationEndpoint }; + + var regDocumentResult = await udapClient.RegisterClientCredentialsClient( + clientCert, + "system/Patient.rs"); + + regDocumentResult.GetError().Should().BeNull(); + + // + // Get Access Token + // + var now = DateTime.UtcNow; + var jwtPayload = new JwtPayLoadExtension( + regDocumentResult.ClientId, + IdentityServerPipeline.TokenEndpoint, + new List() + { + new Claim(JwtClaimTypes.Subject, regDocumentResult.ClientId!), + new Claim(JwtClaimTypes.IssuedAt, EpochTime.GetIntDate(now.ToUniversalTime()).ToString(), + ClaimValueTypes.Integer), + new Claim(JwtClaimTypes.JwtId, CryptoRandom.CreateUniqueId()), + // new Claim(UdapConstants.JwtClaimTypes.Extensions, BuildHl7B2BExtensions() ) //see http://hl7.org/fhir/us/udap-security/b2b.html#constructing-authentication-token + }, + now.ToUniversalTime(), + now.AddMinutes(5).ToUniversalTime() + ); + + var clientAssertion = + SignedSoftwareStatementBuilder + .Create(clientCert, jwtPayload) + .Build("RS384"); + + var jwt = new JsonWebToken(clientAssertion); + var jObject = JObject.Parse(Base64UrlEncoder.Decode(jwt.EncodedHeader)); + jObject.Remove(JwtHeaderParameterNames.Alg); + var header = Base64UrlEncoder.Encode(jObject.ToString()); + clientAssertion = $"{header}.{jwt.EncodedPayload}.{jwt.EncodedSignature}"; + var clientRequest = new UdapClientCredentialsTokenRequest + { + Address = IdentityServerPipeline.TokenEndpoint, + //ClientId = result.ClientId, we use Implicit ClientId in the iss claim + ClientAssertion = new ClientAssertion() + { + Type = OidcConstants.ClientAssertionTypes.JwtBearer, + Value = clientAssertion + }, + Udap = UdapConstants.UdapVersionsSupportedValue, + Scope = "system/Patient.rs" + }; + + + var tokenResponse = await _mockPipeline.BackChannelClient.UdapRequestClientCredentialsTokenAsync(clientRequest); + + tokenResponse.IsError.Should().BeTrue(); + tokenResponse.HttpStatusCode.Should().Be(HttpStatusCode.BadRequest); + tokenResponse.Error.Should().Be("invalid_client"); + tokenResponse.ErrorType.Should().Be(ResponseErrorType.Protocol); + } + + [Fact] + public async Task GetAccessToken_Without_x5c() + { + var clientCert = new X509Certificate2("CertStore/issued/fhirlabs.net.client.pfx", "udap-test"); + + var udapClient = _mockPipeline.Resolve(); + + // + // Typically the client would validate a server before proceeding to registration. + // + udapClient.UdapServerMetaData = new UdapMetadata(Substitute.For(), Substitute.For>()) + { RegistrationEndpoint = UdapAuthServerPipeline.RegistrationEndpoint }; + + var regDocumentResult = await udapClient.RegisterClientCredentialsClient( + clientCert, + "system/Patient.rs"); + + regDocumentResult.GetError().Should().BeNull(); + + // + // Get Access Token + // + var now = DateTime.UtcNow; + var jwtPayload = new JwtPayLoadExtension( + regDocumentResult.ClientId, + IdentityServerPipeline.TokenEndpoint, + new List() + { + new Claim(JwtClaimTypes.Subject, regDocumentResult.ClientId!), + new Claim(JwtClaimTypes.IssuedAt, EpochTime.GetIntDate(now.ToUniversalTime()).ToString(), + ClaimValueTypes.Integer), + new Claim(JwtClaimTypes.JwtId, CryptoRandom.CreateUniqueId()), + // new Claim(UdapConstants.JwtClaimTypes.Extensions, BuildHl7B2BExtensions() ) //see http://hl7.org/fhir/us/udap-security/b2b.html#constructing-authentication-token + }, + now.ToUniversalTime(), + now.AddMinutes(5).ToUniversalTime() + ); + + var clientAssertion = + SignedSoftwareStatementBuilder + .Create(clientCert, jwtPayload) + .Build("RS384"); + + var jwt = new JsonWebToken(clientAssertion); + var jObject = JObject.Parse(Base64UrlEncoder.Decode(jwt.EncodedHeader)); + jObject.Remove(JwtHeaderParameterNames.X5c); + var header = Base64UrlEncoder.Encode(jObject.ToString()); + clientAssertion = $"{header}.{jwt.EncodedPayload}.{jwt.EncodedSignature}"; + var clientRequest = new UdapClientCredentialsTokenRequest + { + Address = IdentityServerPipeline.TokenEndpoint, + //ClientId = result.ClientId, we use Implicit ClientId in the iss claim + ClientAssertion = new ClientAssertion() + { + Type = OidcConstants.ClientAssertionTypes.JwtBearer, + Value = clientAssertion + }, + Udap = UdapConstants.UdapVersionsSupportedValue, + Scope = "system/Patient.rs" + }; + + var tokenResponse = await _mockPipeline.BackChannelClient.UdapRequestClientCredentialsTokenAsync(clientRequest); + + tokenResponse.IsError.Should().BeTrue(); + tokenResponse.HttpStatusCode.Should().Be(HttpStatusCode.BadRequest); + tokenResponse.Error.Should().Be("invalid_client"); + tokenResponse.ErrorType.Should().Be(ResponseErrorType.Protocol); + } + + //Sign with RS384 but set the header alg claim to RS256 + [Fact] + public async Task GetAccessToken_With_invalid_alg() + { + var clientCert = new X509Certificate2("CertStore/issued/fhirlabs.net.client.pfx", "udap-test"); + + var udapClient = _mockPipeline.Resolve(); + + // + // Typically the client would validate a server before proceeding to registration. + // + udapClient.UdapServerMetaData = new UdapMetadata(Substitute.For(), Substitute.For>()) + { RegistrationEndpoint = UdapAuthServerPipeline.RegistrationEndpoint }; + + var regDocumentResult = await udapClient.RegisterClientCredentialsClient( + clientCert, + "system/Patient.rs"); + + regDocumentResult.GetError().Should().BeNull(); + + // + // Get Access Token + // + var now = DateTime.UtcNow; + var jwtPayload = new JwtPayLoadExtension( + regDocumentResult.ClientId, + IdentityServerPipeline.TokenEndpoint, + new List() + { + new Claim(JwtClaimTypes.Subject, regDocumentResult.ClientId!), + new Claim(JwtClaimTypes.IssuedAt, EpochTime.GetIntDate(now.ToUniversalTime()).ToString(), + ClaimValueTypes.Integer), + new Claim(JwtClaimTypes.JwtId, CryptoRandom.CreateUniqueId()), + // new Claim(UdapConstants.JwtClaimTypes.Extensions, BuildHl7B2BExtensions() ) //see http://hl7.org/fhir/us/udap-security/b2b.html#constructing-authentication-token + }, + now.ToUniversalTime(), + now.AddMinutes(5).ToUniversalTime() + ); + + var clientAssertion = + SignedSoftwareStatementBuilder + .Create(clientCert, jwtPayload) + .Build("RS384"); + + var jwt = new JsonWebToken(clientAssertion); + var jObject = JObject.Parse(Base64UrlEncoder.Decode(jwt.EncodedHeader)); + jObject.Property(JwtHeaderParameterNames.Alg)!.Value = "RS256"; // Does not match + + var header = Base64UrlEncoder.Encode(jObject.ToString()); + + var securityKey = new X509SecurityKey(clientCert); + var signingCredentials = new SigningCredentials(securityKey, "RS384"); + var encodedSignature = + JwtTokenUtilities.CreateEncodedSignature(string.Concat(header, ".", jwt.EncodedPayload), + signingCredentials); + + clientAssertion = $"{header}.{jwt.EncodedPayload}.{encodedSignature}"; + + var clientRequest = new UdapClientCredentialsTokenRequest + { + Address = IdentityServerPipeline.TokenEndpoint, + //ClientId = result.ClientId, we use Implicit ClientId in the iss claim + ClientAssertion = new ClientAssertion() + { + Type = OidcConstants.ClientAssertionTypes.JwtBearer, + Value = clientAssertion + }, + Udap = UdapConstants.UdapVersionsSupportedValue, + Scope = "system/Patient.rs" + }; + + + var tokenResponse = await _mockPipeline.BackChannelClient.UdapRequestClientCredentialsTokenAsync(clientRequest); + + tokenResponse.IsError.Should().BeTrue(); + tokenResponse.HttpStatusCode.Should().Be(HttpStatusCode.BadRequest); + tokenResponse.Error.Should().Be("invalid_client"); + tokenResponse.ErrorType.Should().Be(ResponseErrorType.Protocol); + } + + [Fact] + public async Task GetAccessToken_Without_iss() + { + var clientCert = new X509Certificate2("CertStore/issued/fhirlabs.net.client.pfx", "udap-test"); + + var udapClient = _mockPipeline.Resolve(); + + // + // Typically the client would validate a server before proceeding to registration. + // + udapClient.UdapServerMetaData = new UdapMetadata(Substitute.For(), Substitute.For>()) + { RegistrationEndpoint = UdapAuthServerPipeline.RegistrationEndpoint }; + + var regDocumentResult = await udapClient.RegisterClientCredentialsClient( + clientCert, + "system/Patient.rs"); + + regDocumentResult.GetError().Should().BeNull(); + + // + // Get Access Token + // + var now = DateTime.UtcNow; + var jwtPayload = new JwtPayLoadExtension( + regDocumentResult.ClientId, + IdentityServerPipeline.TokenEndpoint, + new List() + { + new Claim(JwtClaimTypes.Subject, regDocumentResult.ClientId!), + new Claim(JwtClaimTypes.IssuedAt, EpochTime.GetIntDate(now.ToUniversalTime()).ToString(), + ClaimValueTypes.Integer), + new Claim(JwtClaimTypes.JwtId, CryptoRandom.CreateUniqueId()), + // new Claim(UdapConstants.JwtClaimTypes.Extensions, BuildHl7B2BExtensions() ) //see http://hl7.org/fhir/us/udap-security/b2b.html#constructing-authentication-token + }, + now.ToUniversalTime(), + now.AddMinutes(5).ToUniversalTime() + ); + + var clientAssertion = + SignedSoftwareStatementBuilder + .Create(clientCert, jwtPayload) + .Build("RS384"); + + var jwt = new JsonWebToken(clientAssertion); + var jObject = JObject.Parse(Base64UrlEncoder.Decode(jwt.EncodedPayload)); + jObject.Remove(JwtClaimTypes.Issuer); + var encodedPayload = Base64UrlEncoder.Encode(jObject.ToString()); + + var securityKey = new X509SecurityKey(clientCert); + var signingCredentials = new SigningCredentials(securityKey, "RS384"); + var encodedSignature = + JwtTokenUtilities.CreateEncodedSignature(string.Concat(jwt.EncodedHeader, ".", encodedPayload), + signingCredentials); + + clientAssertion = $"{jwt.EncodedHeader}.{encodedPayload}.{encodedSignature}"; + + + var clientRequest = new UdapClientCredentialsTokenRequest + { + Address = IdentityServerPipeline.TokenEndpoint, + //ClientId = result.ClientId, we use Implicit ClientId in the iss claim + ClientAssertion = new ClientAssertion() + { + Type = OidcConstants.ClientAssertionTypes.JwtBearer, + Value = clientAssertion + }, + Udap = UdapConstants.UdapVersionsSupportedValue, + Scope = "system/Patient.rs" + }; + + var tokenResponse = await _mockPipeline.BackChannelClient.UdapRequestClientCredentialsTokenAsync(clientRequest); + + tokenResponse.IsError.Should().BeTrue(); + tokenResponse.HttpStatusCode.Should().Be(HttpStatusCode.BadRequest); + tokenResponse.Error.Should().Be("invalid_client"); + tokenResponse.ErrorType.Should().Be(ResponseErrorType.Protocol); + } + [Fact] public async Task GetAccessTokenECDSA() { @@ -281,6 +582,76 @@ public async Task GetAccessTokenECDSA() SignedSoftwareStatementBuilder .Create(clientCert, jwtPayload) .BuildECDSA(); + + var jwt = new JwtSecurityToken(clientAssertion); + jwt.Header.Alg.Should().Be(UdapConstants.SupportedAlgorithm.ES256); + + var clientRequest = new UdapClientCredentialsTokenRequest + { + Address = IdentityServerPipeline.TokenEndpoint, + //ClientId = result.ClientId, we use Implicit ClientId in the iss claim + ClientAssertion = new ClientAssertion() + { + Type = OidcConstants.ClientAssertionTypes.JwtBearer, + Value = clientAssertion + }, + Udap = UdapConstants.UdapVersionsSupportedValue, + Scope = "system/Patient.rs" + }; + + var tokenResponse = await _mockPipeline.BackChannelClient.UdapRequestClientCredentialsTokenAsync(clientRequest); + + tokenResponse.Scope.Should().Be("system/Patient.rs", tokenResponse.Raw); + + } + + [Fact] + public async Task GetAccessTokenECDSA_ES384() + { + var clientCert = new X509Certificate2("CertStore/issued/fhirlabs.net.ecdsa.client.pfx", "udap-test", + X509KeyStorageFlags.Exportable); + + var udapClient = _mockPipeline.Resolve(); + + // + // Typically the client would validate a server before proceeding to registration. + // + udapClient.UdapServerMetaData = new UdapMetadata(Substitute.For(), Substitute.For>()) + { RegistrationEndpoint = UdapAuthServerPipeline.RegistrationEndpoint }; + + var regDocumentResult = await udapClient.RegisterClientCredentialsClient( + clientCert, + "system/Patient.rs"); + + regDocumentResult.GetError().Should().BeNull(); + + + // + // Get Access Token + // + var now = DateTime.UtcNow; + var jwtPayload = new JwtPayLoadExtension( + regDocumentResult!.ClientId, + IdentityServerPipeline.TokenEndpoint, + new List() + { + new Claim(JwtClaimTypes.Subject, regDocumentResult.ClientId!), + new Claim(JwtClaimTypes.IssuedAt, EpochTime.GetIntDate(now.ToUniversalTime()).ToString(), + ClaimValueTypes.Integer), + new Claim(JwtClaimTypes.JwtId, CryptoRandom.CreateUniqueId()), + // new Claim(UdapConstants.JwtClaimTypes.Extensions, BuildHl7B2BExtensions() ) //see http://hl7.org/fhir/us/udap-security/b2b.html#constructing-authentication-token + }, + now.ToUniversalTime(), + now.AddMinutes(5).ToUniversalTime() + ); + + var clientAssertion = + SignedSoftwareStatementBuilder + .Create(clientCert, jwtPayload) + .BuildECDSA(UdapConstants.SupportedAlgorithm.ES384); + + var jwt = new JwtSecurityToken(clientAssertion); + jwt.Header.Alg.Should().Be(UdapConstants.SupportedAlgorithm.ES384); var clientRequest = new UdapClientCredentialsTokenRequest { diff --git a/_tests/UdapServer.Tests/Conformance/Basic/RegistrationAndChangeRegistrationTests.cs b/_tests/UdapServer.Tests/Conformance/Basic/RegistrationAndChangeRegistrationTests.cs index 4aa423c8..59cecd9f 100644 --- a/_tests/UdapServer.Tests/Conformance/Basic/RegistrationAndChangeRegistrationTests.cs +++ b/_tests/UdapServer.Tests/Conformance/Basic/RegistrationAndChangeRegistrationTests.cs @@ -14,8 +14,10 @@ using System.Text.Json; using Duende.IdentityServer.Models; using FluentAssertions; +using FluentAssertions.Common; using IdentityModel; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; using Udap.Client.Configuration; using Udap.Common.Models; using Udap.Model; @@ -45,17 +47,11 @@ public RegistrationAndChangeRegistrationTests(ITestOutputHelper testOutputHelper var anchorCommunity2 = new X509Certificate2("CertStore/anchors/caLocalhostCert2.cer"); var intermediateCommunity2 = new X509Certificate2("CertStore/intermediates/intermediateLocalhostCert2.cer"); - _mockPipeline.OnPostConfigureServices += s => + _mockPipeline.OnPostConfigureServices += services => { - s.AddSingleton(new ServerSettings - { - ServerSupport = ServerSupport.UDAP, - DefaultUserScopes = "udap", - DefaultSystemScopes = "udap", - ForceStateParamOnAuthorizationCode = true - }); - - s.AddSingleton(new UdapClientOptions + services.AddSingleton(sp => sp.GetRequiredService>().Value); + + services.AddSingleton(new UdapClientOptions { ClientName = "Mock Client", Contacts = new HashSet { "mailto:Joseph.Shook@Surescripts.com", "mailto:JoeShook@gmail.com" } diff --git a/_tests/UdapServer.Tests/Conformance/Basic/ReplayRegistrationTests.cs b/_tests/UdapServer.Tests/Conformance/Basic/ReplayRegistrationTests.cs index da5d99da..29a88dc1 100644 --- a/_tests/UdapServer.Tests/Conformance/Basic/ReplayRegistrationTests.cs +++ b/_tests/UdapServer.Tests/Conformance/Basic/ReplayRegistrationTests.cs @@ -45,7 +45,6 @@ public ReplayRegistrationTests(ITestOutputHelper testOutputHelper) { s.AddSingleton(new ServerSettings { - ServerSupport = ServerSupport.UDAP, DefaultUserScopes = "udap", DefaultSystemScopes = "udap", ForceStateParamOnAuthorizationCode = true @@ -64,7 +63,6 @@ public ReplayRegistrationTests(ITestOutputHelper testOutputHelper) { s.AddSingleton(new ServerSettings { - ServerSupport = ServerSupport.UDAP, DefaultUserScopes = "udap", DefaultSystemScopes = "udap", ForceStateParamOnAuthorizationCode = true, diff --git a/_tests/UdapServer.Tests/Conformance/Basic/ScopeExpansionTests.cs b/_tests/UdapServer.Tests/Conformance/Basic/ScopeExpansionTests.cs index 04c19359..451e5955 100644 --- a/_tests/UdapServer.Tests/Conformance/Basic/ScopeExpansionTests.cs +++ b/_tests/UdapServer.Tests/Conformance/Basic/ScopeExpansionTests.cs @@ -16,10 +16,12 @@ using Duende.IdentityServer.Models; using Duende.IdentityServer.Test; using FluentAssertions; +using FluentAssertions.Common; using IdentityModel; using IdentityModel.Client; using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; using Udap.Client.Client.Extensions; using Udap.Client.Configuration; @@ -52,23 +54,22 @@ public ScopeExpansionTests(ITestOutputHelper testOutputHelper) var sureFhirLabsAnchor = new X509Certificate2("CertStore/anchors/SureFhirLabs_CA.cer"); var intermediateCert = new X509Certificate2("CertStore/intermediates/SureFhirLabs_Intermediate.cer"); - _mockPipeline.OnPostConfigureServices += s => + _mockPipeline.OnPostConfigureServices += services => { - s.AddSingleton(new ServerSettings + services.AddSingleton(sp => { - ServerSupport = ServerSupport.UDAP, - DefaultUserScopes = "udap", - DefaultSystemScopes = "udap", - RequireConsent = false + var serverSettings = sp.GetRequiredService>().Value; + serverSettings.RequireConsent = false; + return serverSettings; }); - s.AddSingleton(new UdapClientOptions + services.AddSingleton(new UdapClientOptions { ClientName = "Mock Client", Contacts = new HashSet { "mailto:Joseph.Shook@Surescripts.com", "mailto:JoeShook@gmail.com" } }); - - s.AddScoped(); + + services.AddScoped(); }; _mockPipeline.OnPreConfigureServices += (_, s) => @@ -121,7 +122,7 @@ public ScopeExpansionTests(ITestOutputHelper testOutputHelper) _mockPipeline.IdentityScopes.Add(new IdentityResources.OpenId()); - _mockPipeline.IdentityScopes.Add(new UdapIdentityResources.Udap()); + _mockPipeline.ApiScopes.Add(new UdapApiScopes.Udap()); _mockPipeline.Users.Add(new TestUser { diff --git a/_tests/UdapServer.Tests/Conformance/Basic/UdapForceStateParamFalseTests.cs b/_tests/UdapServer.Tests/Conformance/Basic/UdapForceStateParamFalseTests.cs index e8175d43..592b71d7 100644 --- a/_tests/UdapServer.Tests/Conformance/Basic/UdapForceStateParamFalseTests.cs +++ b/_tests/UdapServer.Tests/Conformance/Basic/UdapForceStateParamFalseTests.cs @@ -57,7 +57,6 @@ public UdapForceStateParamFalseTests(ITestOutputHelper testOutputHelper) { s.AddSingleton(new ServerSettings { - ServerSupport = ServerSupport.Hl7SecurityIG, DefaultUserScopes = "user/*.read", DefaultSystemScopes = "system/*.read", // ForceStateParamOnAuthorizationCode = false (default) @@ -114,7 +113,7 @@ public UdapForceStateParamFalseTests(ITestOutputHelper testOutputHelper) _mockPipeline.IdentityScopes.Add(new IdentityResources.OpenId()); _mockPipeline.IdentityScopes.Add(new IdentityResources.Profile()); - _mockPipeline.IdentityScopes.Add(new UdapIdentityResources.Udap()); + _mockPipeline.ApiScopes.Add(new UdapApiScopes.Udap()); _mockPipeline.Users.Add(new TestUser { diff --git a/_tests/UdapServer.Tests/Conformance/Basic/UdapResponseTypeResponseModeTests.cs b/_tests/UdapServer.Tests/Conformance/Basic/UdapResponseTypeResponseModeTests.cs index 8ceac467..56ff00bd 100644 --- a/_tests/UdapServer.Tests/Conformance/Basic/UdapResponseTypeResponseModeTests.cs +++ b/_tests/UdapServer.Tests/Conformance/Basic/UdapResponseTypeResponseModeTests.cs @@ -57,7 +57,6 @@ public UdapResponseTypeResponseModeTests(ITestOutputHelper testOutputHelper) { s.AddSingleton(new ServerSettings { - ServerSupport = ServerSupport.Hl7SecurityIG, DefaultUserScopes = "user/*.read", DefaultSystemScopes = "system/*.read", ForceStateParamOnAuthorizationCode = true, diff --git a/_tests/UdapServer.Tests/Conformance/Tiered/TieredOauthTests.cs b/_tests/UdapServer.Tests/Conformance/Tiered/TieredOauthTests.cs index 188a0bc7..3489b4bf 100644 --- a/_tests/UdapServer.Tests/Conformance/Tiered/TieredOauthTests.cs +++ b/_tests/UdapServer.Tests/Conformance/Tiered/TieredOauthTests.cs @@ -13,11 +13,14 @@ using System.Security.Claims; using System.Security.Cryptography.X509Certificates; using System.Text; +using System.Text.Encodings.Web; using System.Text.Json; +using System.Web; using Duende.IdentityServer; using Duende.IdentityServer.Models; using Duende.IdentityServer.Test; using FluentAssertions; +using FluentAssertions.Common; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.OpenIdConnect; using Microsoft.AspNetCore.WebUtilities; @@ -64,21 +67,14 @@ public TieredOauthTests(ITestOutputHelper testOutputHelper) _community2Anchor = new X509Certificate2("CertStore/anchors/caLocalhostCert2.cer"); _community2IntermediateCert = new X509Certificate2("CertStore/intermediates/intermediateLocalhostCert2.cer"); - - BuildUdapAuthorizationServer(); - BuildUdapIdentityProvider1(); - BuildUdapIdentityProvider2(); - } + } - private void BuildUdapAuthorizationServer() + private void BuildUdapAuthorizationServer(List? tieredOAuthScopes = null) { _mockAuthorServerPipeline.OnPostConfigureServices += services => { services.AddSingleton(new ServerSettings { - ServerSupport = ServerSupport.Hl7SecurityIG, - // DefaultUserScopes = "udap", - // DefaultSystemScopes = "udap" ForceStateParamOnAuthorizationCode = true, //false (default) RequireConsent = false }); @@ -96,6 +92,18 @@ private void BuildUdapAuthorizationServer() // Allow logo resolve back to udap.auth server // services.AddSingleton(_ => _mockAuthorServerPipeline.BrowserClient); + + if (tieredOAuthScopes != null) + { + services.ConfigureAll(options => + { + options.Scope.Clear(); + foreach (var tieredOAuthScope in tieredOAuthScopes) + { + options.Scope.Add(tieredOAuthScope); + } + }); + } }; _mockAuthorServerPipeline.OnPreConfigureServices += (builderContext, services) => @@ -210,7 +218,7 @@ private void BuildUdapAuthorizationServer() _mockAuthorServerPipeline.IdentityScopes.Add(new IdentityResources.OpenId()); _mockAuthorServerPipeline.IdentityScopes.Add(new IdentityResources.Profile()); - _mockAuthorServerPipeline.IdentityScopes.Add(new UdapIdentityResources.Udap()); + _mockAuthorServerPipeline.ApiScopes.Add(new UdapApiScopes.Udap()); _mockAuthorServerPipeline.ApiScopes.Add(new ApiScope("user/*.read")); @@ -233,15 +241,19 @@ private void BuildUdapIdentityProvider1() { _mockIdPPipeline.OnPostConfigureServices += services => { - services.AddSingleton(new ServerSettings - { - ServerSupport = ServerSupport.UDAP, - DefaultUserScopes = "udap", - DefaultSystemScopes = "udap", - // ForceStateParamOnAuthorizationCode = false (default) - AlwaysIncludeUserClaimsInIdToken = true, - RequireConsent = false - }); + services.AddSingleton( + sp => + { + var serverSettings = sp.GetService>().Value; // must resolve to trigger the post config at TieredIdpServerSettings + serverSettings.DefaultUserScopes = "udap"; + serverSettings.DefaultSystemScopes = "udap"; + // ForceStateParamOnAuthorizationCode = false (default) + serverSettings.AlwaysIncludeUserClaimsInIdToken = true; + serverSettings.RequireConsent = false; + + return serverSettings; + }); + // This registers Clients as List so downstream I can pick it up in InMemoryUdapClientRegistrationStore // Duende's AddInMemoryClients extension registers as IEnumerable and is used in InMemoryClientStore as readonly. @@ -293,7 +305,7 @@ private void BuildUdapIdentityProvider1() _mockIdPPipeline.IdentityScopes.Add(new IdentityResources.OpenId()); _mockIdPPipeline.IdentityScopes.Add(new UdapIdentityResources.Profile()); - _mockIdPPipeline.IdentityScopes.Add(new UdapIdentityResources.Udap()); + _mockIdPPipeline.ApiScopes.Add(new UdapApiScopes.Udap()); _mockIdPPipeline.IdentityScopes.Add(new IdentityResources.Email()); _mockIdPPipeline.IdentityScopes.Add(new UdapIdentityResources.FhirUser()); @@ -318,15 +330,19 @@ private void BuildUdapIdentityProvider2() { _mockIdPPipeline2.OnPostConfigureServices += services => { - services.AddSingleton(new ServerSettings - { - ServerSupport = ServerSupport.UDAP, - DefaultUserScopes = "udap", - DefaultSystemScopes = "udap", - // ForceStateParamOnAuthorizationCode = false (default) - AlwaysIncludeUserClaimsInIdToken = true, - RequireConsent = false - }); + services.AddSingleton( + sp => + { + var serverSettings = sp.GetService>().Value; + serverSettings.DefaultUserScopes = "udap"; + serverSettings.DefaultSystemScopes = "udap"; + // ForceStateParamOnAuthorizationCode = false (default) + serverSettings.AlwaysIncludeUserClaimsInIdToken = true; + serverSettings.RequireConsent = false; + + return serverSettings; + }); + // This registers Clients as List so downstream I can pick it up in InMemoryUdapClientRegistrationStore // Duende's AddInMemoryClients extension registers as IEnumerable and is used in InMemoryClientStore as readonly. @@ -379,7 +395,7 @@ private void BuildUdapIdentityProvider2() _mockIdPPipeline2.IdentityScopes.Add(new IdentityResources.OpenId()); _mockIdPPipeline2.IdentityScopes.Add(new UdapIdentityResources.Profile()); - _mockIdPPipeline2.IdentityScopes.Add(new UdapIdentityResources.Udap()); + _mockIdPPipeline2.ApiScopes.Add(new UdapApiScopes.Udap()); _mockIdPPipeline2.IdentityScopes.Add(new IdentityResources.Email()); _mockIdPPipeline2.IdentityScopes.Add(new UdapIdentityResources.FhirUser()); @@ -407,6 +423,10 @@ private void BuildUdapIdentityProvider2() [Fact] public async Task ClientAuthorize_IdPDiscovery_IdPRegistration_IdPAuthAccess_ClientAuthAccess_Test() { + BuildUdapAuthorizationServer(); + BuildUdapIdentityProvider1(); + + // Register client with auth server var resultDocument = await RegisterClientWithAuthServer(); _mockAuthorServerPipeline.RemoveSessionCookie(); @@ -426,7 +446,8 @@ public async Task ClientAuthorize_IdPDiscovery_IdPRegistration_IdPAuthAccess_Cli // Data Holder's Auth Server validates Identity Provider's Server software statement var clientState = Guid.NewGuid().ToString(); - + + // Builds https://server/connect/authorize plus query params var clientAuthorizeUrl = _mockAuthorServerPipeline.CreateAuthorizeUrl( clientId: clientId, responseType: "code", @@ -515,8 +536,7 @@ public async Task ClientAuthorize_IdPDiscovery_IdPRegistration_IdPAuthAccess_Cli QueryHelpers.ParseQuery(backChannelChallengeResponse.Headers.Location.Query).Single(p => p.Key == "client_id").Value.Should().NotBeEmpty(); var backChannelState = QueryHelpers.ParseQuery(backChannelChallengeResponse.Headers.Location.Query).Single(p => p.Key == "state").Value.ToString(); backChannelState.Should().NotBeNullOrEmpty(); - - + var idpClient = _mockIdPPipeline.Clients.Single(c => c.ClientName == "AuthServer Client"); idpClient.AlwaysIncludeUserClaimsInIdToken.Should().BeTrue(); @@ -697,6 +717,9 @@ public async Task ClientAuthorize_IdPDiscovery_IdPRegistration_IdPAuthAccess_Cli [Fact] //(Skip = "Dynamic Tiered OAuth Provider WIP")] public async Task Tiered_OAuth_With_DynamicProvider() { + BuildUdapAuthorizationServer(); + BuildUdapIdentityProvider2(); + // Register client with auth server var resultDocument = await RegisterClientWithAuthServer(); _mockAuthorServerPipeline.RemoveSessionCookie(); @@ -978,6 +1001,245 @@ public async Task Tiered_OAuth_With_DynamicProvider() } + + /// + /// During Tiered OAuth between the client and data holder the udap scope is required + /// Client call to /authorize? should request with udap scope. + /// Without it the idp is undefined according to https://hl7.org/fhir/us/udap-security/user.html#client-authorization-request-to-data-holder + /// + /// + [Fact] + public async Task ClientAuthorize_Missing_udap_scope_between_client_and_dataholder_Test() + { + BuildUdapAuthorizationServer(); + BuildUdapIdentityProvider1(); + + // Register client with auth server + var resultDocument = await RegisterClientWithAuthServer(); + _mockAuthorServerPipeline.RemoveSessionCookie(); + _mockAuthorServerPipeline.RemoveLoginCookie(); + resultDocument.Should().NotBeNull(); + resultDocument!.ClientId.Should().NotBeNull(); + + var clientId = resultDocument.ClientId!; + + var dynamicIdp = _mockAuthorServerPipeline.ApplicationServices.GetRequiredService(); + dynamicIdp.Name = _mockIdPPipeline.BaseUrl; + + ////////////////////// + // ClientAuthorize + ////////////////////// + + // Data Holder's Auth Server validates Identity Provider's Server software statement + + var clientState = Guid.NewGuid().ToString(); + + var clientAuthorizeUrl = _mockAuthorServerPipeline.CreateAuthorizeUrl( + clientId: clientId, + responseType: "code", + scope: "openid user/*.read", + redirectUri: "https://code_client/callback", + state: clientState, + extra: new + { + idp = "https://idpserver" + }); + + _mockAuthorServerPipeline.BrowserClient.AllowAutoRedirect = false; + // The BrowserHandler.cs will normally set the cookie to indicate user signed in. + // We want to skip that and get a redirect to the login page + _mockAuthorServerPipeline.BrowserClient.AllowCookies = false; + var response = await _mockAuthorServerPipeline.BrowserClient.GetAsync(clientAuthorizeUrl); + response.StatusCode.Should().Be(HttpStatusCode.Redirect, await response.Content.ReadAsStringAsync()); + response.Headers.Location.Should().NotBeNull(); + response.Headers.Location!.AbsoluteUri.Should().Contain("https://server/Account/Login"); + // _testOutputHelper.WriteLine(response.Headers.Location!.AbsoluteUri); + var queryParams = QueryHelpers.ParseQuery(response.Headers.Location.Query); + queryParams.Should().Contain(p => p.Key == "ReturnUrl"); + queryParams.Should().NotContain(p => p.Key == "code"); + queryParams.Should().NotContain(p => p.Key == "state"); + + + // Pull the inner query params from the ReturnUrl + var returnUrl = queryParams.Single(p => p.Key == "ReturnUrl").Value.ToString(); + returnUrl.Should().StartWith("/connect/authorize/callback?"); + queryParams = QueryHelpers.ParseQuery(returnUrl); + queryParams.Single(q => q.Key == "scope").Value.ToString().Should().Contain("openid user/*.read"); + queryParams.Single(q => q.Key == "state").Value.Should().BeEquivalentTo(clientState); + queryParams.Single(q => q.Key == "idp").Value.Should().BeEquivalentTo("https://idpserver"); + + var schemes = await _mockAuthorServerPipeline.Resolve().GetAllSchemesAsync(); + + var sb = new StringBuilder(); + sb.Append("https://server/externallogin/challenge?"); // built in UdapAccount/Login/Index.cshtml.cs + sb.Append("scheme=").Append(schemes.First().Name); + sb.Append("&returnUrl=").Append(Uri.EscapeDataString(returnUrl)); + clientAuthorizeUrl = sb.ToString(); + + + // response after discovery and registration + _mockAuthorServerPipeline.BrowserClient.AllowCookies = true; // Need to set the idsrv cookie so calls to /authorize will succeed + + _mockAuthorServerPipeline.BrowserClient.GetXsrfCookie("https://server/federation/udap-tiered/signin", + new TieredOAuthAuthenticationOptions().CorrelationCookie.Name!).Should().BeNull(); + + var exception = await Assert.ThrowsAsync(() => _mockAuthorServerPipeline.BrowserClient.GetAsync(clientAuthorizeUrl)); + exception.Message.Should().Be("Missing required udap scope from client for Tiered OAuth"); + } + + + /// + /// During Tiered OAuth between data holder and IdP the openid and udap scope are required + /// Client call to /authorize? should request with udap scope. + /// https://hl7.org/fhir/us/udap-security/user.html#data-holder-authentication-request-to-idp + /// + /// + [Theory] + [InlineData(new object[] { new string[] { "openid", "email", "profile"}})] + [InlineData(new object[] { new string[] { "udap", "email", "profile" } })] + public async Task ClientAuthorize_Missing_udap_or_idp_scope_between_dataholder_and_IdP_Test(string[] scopes) + { + // var scopes = new List() { "email", "profile" }; + BuildUdapAuthorizationServer(scopes.ToList()); + BuildUdapIdentityProvider1(); + + // Register client with auth server + var resultDocument = await RegisterClientWithAuthServer(); + _mockAuthorServerPipeline.RemoveSessionCookie(); + _mockAuthorServerPipeline.RemoveLoginCookie(); + resultDocument.Should().NotBeNull(); + resultDocument!.ClientId.Should().NotBeNull(); + + var clientId = resultDocument.ClientId!; + + var dynamicIdp = _mockAuthorServerPipeline.ApplicationServices.GetRequiredService(); + dynamicIdp.Name = _mockIdPPipeline.BaseUrl; + + ////////////////////// + // ClientAuthorize + ////////////////////// + + // Data Holder's Auth Server validates Identity Provider's Server software statement + + var clientState = Guid.NewGuid().ToString(); + + var clientAuthorizeUrl = _mockAuthorServerPipeline.CreateAuthorizeUrl( + clientId: clientId, + responseType: "code", + scope: "udap openid user/*.read", + redirectUri: "https://code_client/callback", + state: clientState, + extra: new + { + idp = "https://idpserver" + }); + + _mockAuthorServerPipeline.BrowserClient.AllowAutoRedirect = false; + // The BrowserHandler.cs will normally set the cookie to indicate user signed in. + // We want to skip that and get a redirect to the login page + _mockAuthorServerPipeline.BrowserClient.AllowCookies = false; + var response = await _mockAuthorServerPipeline.BrowserClient.GetAsync(clientAuthorizeUrl); + response.StatusCode.Should().Be(HttpStatusCode.Redirect, await response.Content.ReadAsStringAsync()); + response.Headers.Location.Should().NotBeNull(); + response.Headers.Location!.AbsoluteUri.Should().Contain("https://server/Account/Login"); + // _testOutputHelper.WriteLine(response.Headers.Location!.AbsoluteUri); + var queryParams = QueryHelpers.ParseQuery(response.Headers.Location.Query); + queryParams.Should().Contain(p => p.Key == "ReturnUrl"); + queryParams.Should().NotContain(p => p.Key == "code"); + queryParams.Should().NotContain(p => p.Key == "state"); + + + // Pull the inner query params from the ReturnUrl + var returnUrl = queryParams.Single(p => p.Key == "ReturnUrl").Value.ToString(); + returnUrl.Should().StartWith("/connect/authorize/callback?"); + queryParams = QueryHelpers.ParseQuery(returnUrl); + queryParams.Single(q => q.Key == "scope").Value.ToString().Should().Contain("udap openid user/*.read"); + queryParams.Single(q => q.Key == "state").Value.Should().BeEquivalentTo(clientState); + queryParams.Single(q => q.Key == "idp").Value.Should().BeEquivalentTo("https://idpserver"); + + var schemes = await _mockAuthorServerPipeline.Resolve().GetAllSchemesAsync(); + + var sb = new StringBuilder(); + sb.Append("https://server/externallogin/challenge?"); // built in UdapAccount/Login/Index.cshtml.cs + sb.Append("scheme=").Append(schemes.First().Name); + sb.Append("&returnUrl=").Append(Uri.EscapeDataString(returnUrl)); + clientAuthorizeUrl = sb.ToString(); + + + + ////////////////////////////////// + // + // IdPDiscovery + // IdPRegistration + // IdPAuthAccess + // + ////////////////////////////////// + + + // Auto Dynamic registration between Auth Server and Identity Provider happens here. + // /Challenge? + // ctx.ChallengeAsync -> launch registered scheme. In this case the TieredOauthAuthenticationHandler + // see: OnExternalLoginChallenge and Challenge(props, scheme) in ExternalLogin/Challenge.cshtml.cs or UdapTieredLogin/Challenge.cshtml.cs + // Backchannel + // Discovery + // Auto registration + // externalloging/challenge or in the Udap implementation it is the UdapAccount/Login/Index.cshtml.cs. XSRF cookie is set here. + + // *** We are here after the request to the IdPs /authorize call. If the client is registered already then Discovery and Reg is skipped *** + // + // Authentication request (/authorize?) + // User logs in at IdP + // Authentication response + // Token request + // Data Holder incorporates user input into authorization decision + // + + + + // response after discovery and registration + _mockAuthorServerPipeline.BrowserClient.AllowCookies = + true; // Need to set the idsrv cookie so calls to /authorize will succeed + + _mockAuthorServerPipeline.BrowserClient.GetXsrfCookie("https://server/federation/udap-tiered/signin", + new TieredOAuthAuthenticationOptions().CorrelationCookie.Name!).Should().BeNull(); + var backChannelChallengeResponse = await _mockAuthorServerPipeline.BrowserClient.GetAsync(clientAuthorizeUrl); + _mockAuthorServerPipeline.BrowserClient.GetXsrfCookie("https://server/federation/udap-tiered/signin", + new TieredOAuthAuthenticationOptions().CorrelationCookie.Name!).Should().NotBeNull(); + + backChannelChallengeResponse.StatusCode.Should().Be(HttpStatusCode.Redirect, + await backChannelChallengeResponse.Content.ReadAsStringAsync()); + backChannelChallengeResponse.Headers.Location.Should().NotBeNull(); + backChannelChallengeResponse.Headers.Location!.AbsoluteUri.Should() + .StartWith("https://idpserver/connect/authorize"); + + // _testOutputHelper.WriteLine(backChannelChallengeResponse.Headers.Location!.AbsoluteUri); + QueryHelpers.ParseQuery(backChannelChallengeResponse.Headers.Location.Query).Single(p => p.Key == "client_id") + .Value.Should().NotBeEmpty(); + var backChannelState = QueryHelpers.ParseQuery(backChannelChallengeResponse.Headers.Location.Query) + .Single(p => p.Key == "state").Value.ToString(); + backChannelState.Should().NotBeNullOrEmpty(); + + var idpClient = _mockIdPPipeline.Clients.Single(c => c.ClientName == "AuthServer Client"); + idpClient.AlwaysIncludeUserClaimsInIdToken.Should().BeTrue(); + + + Debug.Assert(_mockIdPPipeline.BrowserClient != null, "_mockIdPPipeline.BrowserClient != null"); + var backChannelAuthResult = + await _mockIdPPipeline.BrowserClient.GetAsync(backChannelChallengeResponse.Headers.Location); + _testOutputHelper.WriteLine(HttpUtility.UrlDecode(backChannelAuthResult.Headers.Location.Query)); + + backChannelAuthResult.StatusCode.Should().Be(HttpStatusCode.Redirect, + await backChannelAuthResult.Content.ReadAsStringAsync()); + backChannelAuthResult.Headers.Location.Should().NotBeNull(); + backChannelAuthResult.Headers.Location!.AbsoluteUri.Should() + .StartWith("https://server/federation/udap-tiered/signin"); //signin callback scheme + + var responseParams = QueryHelpers.ParseQuery(backChannelAuthResult.Headers.Location.Query); + responseParams["error"].Should().BeEquivalentTo("invalid_request"); + responseParams["error_description"].Should().BeEquivalentTo("Missing udap and/or openid scope between data holder and IdP"); + responseParams["scope"].Should().BeEquivalentTo(scopes.ToSpaceSeparatedString()); + } + private async Task RegisterClientWithAuthServer() { var clientCert = new X509Certificate2("CertStore/issued/fhirLabsApiClientLocalhostCert.pfx", "udap-test"); diff --git a/_tests/UdapServer.Tests/Hl7RegistrationTests.cs b/_tests/UdapServer.Tests/Hl7RegistrationTests.cs index c4f5c2f9..3cea2628 100644 --- a/_tests/UdapServer.Tests/Hl7RegistrationTests.cs +++ b/_tests/UdapServer.Tests/Hl7RegistrationTests.cs @@ -93,8 +93,7 @@ protected override IHost CreateHost(IHostBuilder builder) var overrideSettings = new Dictionary { - { "ConnectionStrings:DefaultConnection", "Data Source=Udap.Idp.db.HL7;" }, - { "ServerSettings:ServerSupport", "Hl7SecurityIG" } + { "ConnectionStrings:DefaultConnection", "Data Source=Udap.Idp.db.HL7;" } }; builder.ConfigureHostConfiguration(b => b.AddInMemoryCollection(overrideSettings!)); diff --git a/_tests/UdapServer.Tests/SeedData.cs b/_tests/UdapServer.Tests/SeedData.cs index fc48adb9..030121f3 100644 --- a/_tests/UdapServer.Tests/SeedData.cs +++ b/_tests/UdapServer.Tests/SeedData.cs @@ -226,7 +226,6 @@ private static async Task SeedFhirScopes( { var apiScopes = configDbContext.ApiScopes .Include(s => s.Properties) - .Where(s => s.Enabled) .Select(s => s) .ToList(); @@ -241,6 +240,7 @@ private static async Task SeedFhirScopes( if (apiScope.Name.StartsWith("system/*.")) { apiScope.ShowInDiscoveryDocument = true; + apiScope.Enabled = false; } apiScope.Properties.Add("udap_prefix", "system"); @@ -262,9 +262,10 @@ private static async Task SeedFhirScopes( var apiScope = new ApiScope(scopeName); apiScope.ShowInDiscoveryDocument = false; - if (apiScope.Name.StartsWith("patient/*.")) + if (apiScope.Name.StartsWith("user/*.")) { apiScope.ShowInDiscoveryDocument = true; + apiScope.Enabled = false; } apiScope.Properties.Add("udap_prefix", "user"); @@ -288,6 +289,7 @@ private static async Task SeedFhirScopes( if (apiScope.Name.StartsWith("patient/*.")) { apiScope.ShowInDiscoveryDocument = true; + apiScope.Enabled = false; } apiScope.Properties.Add("udap_prefix", "patient"); diff --git a/_tests/UdapServer.Tests/UdapRegistrationTests.cs b/_tests/UdapServer.Tests/UdapRegistrationTests.cs deleted file mode 100644 index 1dc4bef9..00000000 --- a/_tests/UdapServer.Tests/UdapRegistrationTests.cs +++ /dev/null @@ -1,1375 +0,0 @@ -#region (c) 2023 Joseph Shook. All rights reserved. -// /* -// Authors: -// Joseph Shook Joseph.Shook@Surescripts.com -// -// See LICENSE in the project root for license information. -// */ -#endregion - -using System.Net; -using System.Net.Http.Json; -using System.Security.Cryptography.X509Certificates; -using System.Text; -using System.Text.Json; -using Duende.IdentityServer.EntityFramework.DbContexts; -using FluentAssertions; -using Hl7.Fhir.Model; -using IdentityModel; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Mvc.Testing; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using Microsoft.IdentityModel.Tokens; -using NSubstitute; -using Udap.Client.Client.Extensions; -using Udap.Common.Certificates; -using Udap.Model; -using Udap.Model.Registration; -using Udap.Model.Statement; -using Udap.Server.DbContexts; -using Xunit.Abstractions; -using Task = System.Threading.Tasks.Task; - -namespace UdapServer.Tests; - -public class UdapApiTestFixture : WebApplicationFactory -{ - public ITestOutputHelper? Output { get; set; } - public IUdapDbAdminContext UdapDbAdminContext { get; set; } = null!; - - private ServiceProvider _serviceProvider = null!; - private IServiceScope _serviceScope = null!; - - - public UdapApiTestFixture() - { - SeedData.EnsureSeedData("Data Source=./Udap.Idp.db;", Substitute.For()).GetAwaiter().GetResult(); - } - - protected override IHost CreateHost(IHostBuilder builder) - { - Environment.SetEnvironmentVariable("ASPNETCORE_URLS", "http://localhost"); - //Similar to pushing to the cloud where the docker image runs as localhost:8080 but we want to inform Udap.Idp - //that it is some other https url for settings like aud, register and other metadata published settings. - Environment.SetEnvironmentVariable("UdapIdpBaseUrl", "http://localhost"); - Environment.SetEnvironmentVariable("provider", "Sqlite"); - builder.UseEnvironment("Development"); - - builder.ConfigureServices(services => - { - services.AddSingleton(); - - // - // Fix-up TrustChainValidator to ignore certificate revocation - // - var descriptor = services.SingleOrDefault(d => d.ServiceType == typeof(TrustChainValidator)); - - - if (descriptor != null) - { - Console.WriteLine($"Removing {descriptor}"); - services.Remove(descriptor); - } - else - { - Console.WriteLine("Nothing to remove???"); - } - - services.AddSingleton(new TrustChainValidator( - new X509ChainPolicy - { - VerificationFlags = X509VerificationFlags.IgnoreWrongUsage, - RevocationFlag = X509RevocationFlag.ExcludeRoot, - RevocationMode = X509RevocationMode.NoCheck // This is the change unit testing with no revocation endpoint to host the revocation list. - }, - Output!.ToLogger())); - - _serviceProvider = services.BuildServiceProvider(); - _serviceScope = _serviceProvider.GetRequiredService().CreateScope(); - UdapDbAdminContext = _serviceScope.ServiceProvider.GetRequiredService(); - }); - - var overrideSettings = new Dictionary - { - { "ConnectionStrings:DefaultConnection", "Data Source=Udap.Idp.db;" }, - { "ServerSettings:ServerSupport", "UDAP"}, - { "ServerSettings:LogoRequired", "false"} - - }; - - var sb = new StringBuilder(); - - foreach (var resName in ModelInfo.SupportedResources) - { - sb.Append(' ').Append($"user/{resName}.*"); - sb.Append(' ').Append($"user/{resName}.read"); - } - - overrideSettings.Add("ServerSettings:DefaultUserScopes", sb.ToString().TrimStart()); - - sb = new StringBuilder(); - - foreach (var resName in ModelInfo.SupportedResources) - { - sb.Append(' ').Append($"system/{resName}.*"); - sb.Append(' ').Append($"system/{resName}.read"); - } - - overrideSettings.Add("ServerSettings:DefaultSystemScopes", sb.ToString().TrimStart()); - - - - builder.ConfigureHostConfiguration(b => b.AddInMemoryCollection(overrideSettings!)); - - builder.ConfigureLogging(logging => - { - logging.ClearProviders(); - logging.AddXUnit(Output!); - }); - - var app = base.CreateHost(builder); - - return app; - } - - /// - public override async ValueTask DisposeAsync() - { - _serviceScope.Dispose(); - await _serviceProvider.DisposeAsync(); - } - - protected override void ConfigureWebHost(IWebHostBuilder builder) - { - builder.UseSetting("skipRateLimiting", null); - - // - // Linux needs to know how to find appsettings file in web api under test. - // Still works with Windows but what a pain. This feels fragile - // TODO: - // - //This is not working for linux tests like it did in other projects. - builder.UseSetting("contentRoot", "../../../../../examples/Udap.Auth.Server/"); - } -} - -/// -/// Full Web tests. Using web server. -/// -[Collection("Udap.Auth.Server")] -public class UdapServerRegistrationTests : IClassFixture -{ - private UdapApiTestFixture _fixture; - private readonly ITestOutputHelper _testOutputHelper; - - public UdapServerRegistrationTests(UdapApiTestFixture fixture, ITestOutputHelper testOutputHelper) - { - if (fixture == null) throw new ArgumentNullException(nameof(fixture)); - fixture.Output = testOutputHelper; - _fixture = fixture; - _testOutputHelper = testOutputHelper; - } - - [Fact] - public async Task RegistrationSuccess_authorization_code_Test() - { - using var client = _fixture.CreateClient(); - await ResetClientInDatabase(); - - var disco = await client.GetUdapDiscoveryDocument(); - - disco.HttpResponse.StatusCode.Should().Be(HttpStatusCode.OK); - disco.IsError.Should().BeFalse($"{disco.Error} :: {disco.HttpErrorReason}"); - // var discoJsonFormatted = - // JsonSerializer.Serialize(disco.Json, new JsonSerializerOptions { WriteIndented = true }); - // _testOutputHelper.WriteLine(discoJsonFormatted); - var regEndpoint = disco.RegistrationEndpoint; - var reg = new Uri(regEndpoint!); - - var cert = Path.Combine("CertStore/issued", - "weatherApiClientLocalhostCert1.pfx"); - - _testOutputHelper.WriteLine($"Path to Cert: {cert}"); - var clientCert = new X509Certificate2(cert, "udap-test"); - var now = DateTime.UtcNow; - var jwtId = CryptoRandom.CreateUniqueId(); - // - // Could use JwtPayload. But because we have a typed object, UdapDynamicClientRegistrationDocument - // I have it implementing IDictionary so the JsonExtensions.SerializeToJson method - // can prepare it the same way JwtPayLoad is essentially implemented, but light weight - // and specific to this Udap Dynamic Registration. - // - - var document = new UdapDynamicClientRegistrationDocument - { - Issuer = "http://localhost/", - Subject = "http://localhost/", - Audience = "http://localhost/connect/register", - Expiration = EpochTime.GetIntDate(now.AddMinutes(1).ToUniversalTime()), - IssuedAt = EpochTime.GetIntDate(now.ToUniversalTime()), - JwtId = jwtId, - ClientName = "udapTestClient", - Contacts = new HashSet { "FhirJoe@BridgeTown.lab", "FhirJoe@test.lab" }, - GrantTypes = new HashSet { "authorization_code", "refresh_token" }, - ResponseTypes = new HashSet { "code" }, - RedirectUris = new List(){ "http://localhost/signin-oidc" }, - TokenEndpointAuthMethod = UdapConstants.RegistrationDocumentValues.TokenEndpointAuthMethodValue, - Scope = "user/Patient.*" - }; - - document.Add("Extra", "Stuff" as string); - - var signedSoftwareStatement = - SignedSoftwareStatementBuilder - .Create(clientCert, document) - .Build(); - - var requestBody = new UdapRegisterRequest - ( - signedSoftwareStatement, - UdapConstants.UdapVersionsSupportedValue - ); - - var response = - await client.PostAsJsonAsync(reg, - requestBody); //TODO on server side fail for Certifications empty collection - - if (response.StatusCode != HttpStatusCode.Created) - { - _testOutputHelper.WriteLine(await response.Content.ReadAsStringAsync()); - } - - response.StatusCode.Should().Be(HttpStatusCode.Created); - - // var documentAsJson = JsonSerializer.Serialize(document); - // var result = await response.Content.ReadAsStringAsync(); - // _testOutputHelper.WriteLine(result); - // result.Should().BeEquivalentTo(documentAsJson); - - var responseUdapDocument = - await response.Content.ReadFromJsonAsync(); - - responseUdapDocument.Should().NotBeNull(); - responseUdapDocument!.ClientId.Should().NotBeNullOrEmpty(); - _testOutputHelper.WriteLine(JsonSerializer.Serialize(responseUdapDocument, - new JsonSerializerOptions { WriteIndented = true })); - - // - // Assertions according to - // https://datatracker.ietf.org/doc/html/rfc7591#section-3.2.1 - // - responseUdapDocument.SoftwareStatement.Should().Be(signedSoftwareStatement); - responseUdapDocument.ClientName.Should().Be(document.ClientName); - responseUdapDocument.Issuer.Should().Be(document.Issuer); - - ((JsonElement)responseUdapDocument["Extra"]).GetString().Should().Be(document["Extra"].ToString()); - - - using var scope = _fixture.Services.GetRequiredService().CreateScope(); - var udapContext = scope.ServiceProvider.GetRequiredService(); - - var clientEntity = await udapContext.Clients - .Include(c => c.RedirectUris) - .SingleAsync(c => c.ClientId == responseUdapDocument.ClientId); - - clientEntity.RequirePkce.Should().BeFalse(); - - clientEntity.RedirectUris.Single().RedirectUri.Should().Be("http://localhost/signin-oidc"); - clientEntity.AllowOfflineAccess.Should().BeTrue(); - } - - [Fact] - public async Task RegistrationSuccessTest() - { - using var client = _fixture.CreateClient(); - await ResetClientInDatabase(); - - var disco = await client.GetUdapDiscoveryDocument(); - - disco.HttpResponse.StatusCode.Should().Be(HttpStatusCode.OK); - disco.IsError.Should().BeFalse($"{disco.Error} :: {disco.HttpErrorReason}"); - // var discoJsonFormatted = - // JsonSerializer.Serialize(disco.Json, new JsonSerializerOptions { WriteIndented = true }); - // _testOutputHelper.WriteLine(discoJsonFormatted); - var regEndpoint = disco.RegistrationEndpoint; - var reg = new Uri(regEndpoint!); - - var cert = Path.Combine("CertStore/issued", "weatherApiClientLocalhostCert1.pfx"); - - _testOutputHelper.WriteLine($"Path to Cert: {cert}"); - var clientCert = new X509Certificate2(cert, "udap-test"); - var now = DateTime.UtcNow; - var jwtId = CryptoRandom.CreateUniqueId(); - - var document = new UdapDynamicClientRegistrationDocument - { - Issuer = "http://localhost/", - Subject = "http://localhost/", - Audience = "https://localhost/connect/register", - Expiration = EpochTime.GetIntDate(now.AddMinutes(1).ToUniversalTime()), - IssuedAt = EpochTime.GetIntDate(now.ToUniversalTime()), - JwtId = jwtId, - ClientName = "udapTestClient", - Contacts = new HashSet { "FhirJoe@BridgeTown.lab", "FhirJoe@test.lab" }, - GrantTypes = new HashSet { "client_credentials" }, - TokenEndpointAuthMethod = UdapConstants.RegistrationDocumentValues.TokenEndpointAuthMethodValue - }; - - document.Add("Extra", "Stuff" as string); - - var signedSoftwareStatement = - SignedSoftwareStatementBuilder - .Create(clientCert, document) - .Build(); - - var requestBody = new UdapRegisterRequest - ( - signedSoftwareStatement, - UdapConstants.UdapVersionsSupportedValue - ); - - var response = - await client.PostAsJsonAsync(reg, - requestBody); //TODO on server side fail for Certifications empty collection - - if (response.StatusCode != HttpStatusCode.Created) - { - _testOutputHelper.WriteLine(await response.Content.ReadAsStringAsync()); - } - - response.StatusCode.Should().Be(HttpStatusCode.Created); - - // var documentAsJson = JsonSerializer.Serialize(document); - // var result = await response.Content.ReadAsStringAsync(); - // _testOutputHelper.WriteLine(result); - // result.Should().BeEquivalentTo(documentAsJson); - - var responseUdapDocument = - await response.Content.ReadFromJsonAsync(); - - responseUdapDocument.Should().NotBeNull(); - responseUdapDocument!.ClientId.Should().NotBeNullOrEmpty(); - _testOutputHelper.WriteLine(JsonSerializer.Serialize(responseUdapDocument, - new JsonSerializerOptions { WriteIndented = true })); - - // - // Assertions according to - // https://datatracker.ietf.org/doc/html/rfc7591#section-3.2.1 - // - responseUdapDocument.SoftwareStatement.Should().Be(signedSoftwareStatement); - responseUdapDocument.ClientName.Should().Be(document.ClientName); - responseUdapDocument.Issuer.Should().Be(document.Issuer); - - ((JsonElement)responseUdapDocument["Extra"]).GetString().Should().Be(document["Extra"].ToString()); - - - using var scope = _fixture.Services.GetRequiredService().CreateScope(); - var udapContext = scope.ServiceProvider.GetRequiredService(); - - var clientEntity = await udapContext.Clients - .Include(c => c.AllowedScopes) - .SingleAsync(c => c.ClientId == responseUdapDocument.ClientId); - - clientEntity.RequirePkce.Should().BeTrue(); - - clientEntity.AllowedScopes.Count.Should().Be(ModelInfo.SupportedResources.Count * 2); - clientEntity.AllowOfflineAccess.Should().BeFalse(); - } - - [Fact] - public async Task RegistrationMissingx5cHeaderTest() - { - // var clientPolicyStore = _fixture.Services.GetService(); - // - // - using var client = _fixture.CreateClient(); - var disco = await client.GetUdapDiscoveryDocument(); - - disco.HttpResponse.StatusCode.Should().Be(HttpStatusCode.OK); - disco.IsError.Should().BeFalse($"{disco.Error} :: {disco.HttpErrorReason}"); - // var discoJsonFormatted = - // JsonSerializer.Serialize(disco.Json, new JsonSerializerOptions { WriteIndented = true }); - // _testOutputHelper.WriteLine(discoJsonFormatted); - var regEndpoint = disco.RegistrationEndpoint; - var reg = new Uri(regEndpoint!); - - var cert = Path.Combine(Path.Combine(AppContext.BaseDirectory, "CertStore/issued"), - "weatherApiClientLocalhostCert1.pfx"); - - var clientCert = new X509Certificate2(cert, "udap-test"); - var now = DateTime.UtcNow; - var jwtId = CryptoRandom.CreateUniqueId(); - - var document = new UdapDynamicClientRegistrationDocument - { - Issuer = "https://weatherapi.lab:5021/fhir", - Subject = "https://weatherapi.lab:5021/fhir", - Audience = "https://weatherapi.lab:5021/connect/register", - Expiration = EpochTime.GetIntDate(now.AddMinutes(1).ToUniversalTime()), - IssuedAt = EpochTime.GetIntDate(now.ToUniversalTime()), - JwtId = jwtId, - ClientName = "udapTestClient", - Contacts = new HashSet { "FhirJoe@BridgeTown.lab", "FhirJoe@test.lab" }, - GrantTypes = new HashSet { "client_credentials" }, - TokenEndpointAuthMethod = UdapConstants.RegistrationDocumentValues.TokenEndpointAuthMethodValue, - Scope = "system/Patient.* system/Practitioner.read" - }; - - document.Add("Extra", "Stuff" as string); - - var signedSoftwareStatement = - SignedSoftwareStatementBuilder - .Create(clientCert, document) - .Build(); - - var requestBody = new UdapRegisterRequest - ( - signedSoftwareStatement, - UdapConstants.UdapVersionsSupportedValue - ); - - var response = await client.PostAsJsonAsync(reg, requestBody); - - if (response.StatusCode != HttpStatusCode.Created) - { - _testOutputHelper.WriteLine(await response.Content.ReadAsStringAsync()); - } - - response.StatusCode.Should().Be(HttpStatusCode.BadRequest); - - var errorResponse = - await response.Content.ReadFromJsonAsync(); - - errorResponse.Should().NotBeNull(); - errorResponse!.Error.Should().Be(UdapDynamicClientRegistrationErrors.InvalidSoftwareStatement); - } - - //invalid_software_statement - [Fact] - public async Task RegistrationInvalidSoftwareStatement_Signature_Test() - { - using var client = _fixture.CreateClient(); - var disco = await client.GetUdapDiscoveryDocument(); - - disco.HttpResponse.StatusCode.Should().Be(HttpStatusCode.OK); - disco.IsError.Should().BeFalse($"{disco.Error} :: {disco.HttpErrorReason}"); - - var regEndpoint = disco.RegistrationEndpoint; - var reg = new Uri(regEndpoint!); - - var cert = Path.Combine(Path.Combine(AppContext.BaseDirectory, "CertStore/issued"), - "weatherApiClientLocalhostCert1.pfx"); - - var clientCert = new X509Certificate2(cert, "udap-test"); - var now = DateTime.UtcNow; - var jwtId = CryptoRandom.CreateUniqueId(); - - var document = new UdapDynamicClientRegistrationDocument - { - Issuer = "http://localhost/", - Subject = "http://localhost/", - Audience = "https://localhost:5002/connect/register", - Expiration = EpochTime.GetIntDate(now.AddMinutes(1).ToUniversalTime()), - IssuedAt = EpochTime.GetIntDate(now.ToUniversalTime()), - JwtId = jwtId, - ClientName = "udapTestClient", - Contacts = new HashSet { "FhirJoe@BridgeTown.lab", "FhirJoe@test.lab" }, - GrantTypes = new HashSet { "client_credentials" }, - TokenEndpointAuthMethod = UdapConstants.RegistrationDocumentValues.TokenEndpointAuthMethodValue, - Scope = "system/Patient.* system/Practitioner.read" - }; - - document.Add("Extra", "Stuff" as string); - - var signedSoftwareStatement = - SignedSoftwareStatementBuilder - .Create(clientCert, document) - .Build(); - - var requestBody = new UdapRegisterRequest - ( - signedSoftwareStatement + "Invalid", - UdapConstants.UdapVersionsSupportedValue - ); - - var response = await client.PostAsJsonAsync(reg, requestBody); - - if (response.StatusCode != HttpStatusCode.Created) - { - _testOutputHelper.WriteLine(await response.Content.ReadAsStringAsync()); - } - - response.StatusCode.Should().Be(HttpStatusCode.BadRequest); - - var errorResponse = - await response.Content.ReadFromJsonAsync(); - - errorResponse.Should().NotBeNull(); - errorResponse!.Error.Should().Be(UdapDynamicClientRegistrationErrors.InvalidSoftwareStatement); - } - - //invalid_software_statement - [Fact] - public async Task RegisrationInvalidSotwareStatement_issMatchesUriName_Test() - { - using var client = _fixture.CreateClient(); - var disco = await client.GetUdapDiscoveryDocument(); - - disco.HttpResponse.StatusCode.Should().Be(HttpStatusCode.OK); - disco.IsError.Should().BeFalse($"{disco.Error} :: {disco.HttpErrorReason}"); - - var regEndpoint = disco.RegistrationEndpoint; - var reg = new Uri(regEndpoint!); - - var cert = Path.Combine(Path.Combine(AppContext.BaseDirectory, "CertStore/issued"), - "weatherApiClientLocalhostCert1.pfx"); - - var clientCert = new X509Certificate2(cert, "udap-test"); - var now = DateTime.UtcNow; - var jwtId = CryptoRandom.CreateUniqueId(); - - var document = new UdapDynamicClientRegistrationDocument - { - Issuer = "http://localhost:9999/", - Subject = "http://localhost/", - Audience = "https://localhost:5002/connect/register", - Expiration = EpochTime.GetIntDate(now.AddMinutes(1).ToUniversalTime()), - IssuedAt = EpochTime.GetIntDate(now.ToUniversalTime()), - JwtId = jwtId, - ClientName = "udapTestClient", - Contacts = new HashSet { "FhirJoe@BridgeTown.lab", "FhirJoe@test.lab" }, - GrantTypes = new HashSet { "client_credentials" }, - - TokenEndpointAuthMethod = UdapConstants.RegistrationDocumentValues.TokenEndpointAuthMethodValue, - Scope = "system/Patient.* system/Practitioner.read" - }; - - document.Add("Extra", "Stuff" as string); - - var signedSoftwareStatement = - SignedSoftwareStatementBuilder - .Create(clientCert, document) - .Build(); - - var requestBody = new UdapRegisterRequest - ( - signedSoftwareStatement, - UdapConstants.UdapVersionsSupportedValue - ); - - var response = await client.PostAsJsonAsync(reg, requestBody); - - if (response.StatusCode != HttpStatusCode.Created) - { - _testOutputHelper.WriteLine(await response.Content.ReadAsStringAsync()); - } - - response.StatusCode.Should().Be(HttpStatusCode.BadRequest); - - var errorResponse = - await response.Content.ReadFromJsonAsync(); - - errorResponse.Should().NotBeNull(); - errorResponse!.Error.Should().Be(UdapDynamicClientRegistrationErrors.InvalidSoftwareStatement); - } - - //invalid_software_statement - [Fact] - public async Task RegisrationInvalidSotwareStatement_issMissing_Test() - { - using var client = _fixture.CreateClient(); - var disco = await client.GetUdapDiscoveryDocument(); - - disco.HttpResponse.StatusCode.Should().Be(HttpStatusCode.OK); - disco.IsError.Should().BeFalse($"{disco.Error} :: {disco.HttpErrorReason}"); - - var regEndpoint = disco.RegistrationEndpoint; - var reg = new Uri(regEndpoint!); - - var cert = Path.Combine(Path.Combine(AppContext.BaseDirectory, "CertStore/issued"), - "weatherApiClientLocalhostCert1.pfx"); - - var clientCert = new X509Certificate2(cert, "udap-test"); - var now = DateTime.UtcNow; - var jwtId = CryptoRandom.CreateUniqueId(); - - var document = new UdapDynamicClientRegistrationDocument - { - // Issuer = "http://localhost/", - Subject = "http://localhost/", - Audience = "https://localhost:5002/connect/register", - Expiration = EpochTime.GetIntDate(now.AddMinutes(1).ToUniversalTime()), - IssuedAt = EpochTime.GetIntDate(now.ToUniversalTime()), - JwtId = jwtId, - ClientName = "udapTestClient", - Contacts = new HashSet { "FhirJoe@BridgeTown.lab", "FhirJoe@test.lab" }, - GrantTypes = new HashSet { "client_credentials" }, - TokenEndpointAuthMethod = UdapConstants.RegistrationDocumentValues.TokenEndpointAuthMethodValue, - Scope = "system/Patient.* system/Practitioner.read" - }; - - document.Add("Extra", "Stuff" as string); - - var signedSoftwareStatement = - SignedSoftwareStatementBuilder - .Create(clientCert, document) - .Build(); - - var requestBody = new UdapRegisterRequest - ( - signedSoftwareStatement, - UdapConstants.UdapVersionsSupportedValue - ); - - var response = await client.PostAsJsonAsync(reg, requestBody); - - if (response.StatusCode != HttpStatusCode.Created) - { - _testOutputHelper.WriteLine(await response.Content.ReadAsStringAsync()); - } - - response.StatusCode.Should().Be(HttpStatusCode.BadRequest); - - var errorResponse = - await response.Content.ReadFromJsonAsync(); - - errorResponse.Should().NotBeNull(); - errorResponse!.Error.Should().Be(UdapDynamicClientRegistrationErrors.InvalidSoftwareStatement); - } - - //invalid_software_statement - [Fact] - public async Task RegisrationInvalidSotwareStatement_subMissing_Test() - { - using var client = _fixture.CreateClient(); - var disco = await client.GetUdapDiscoveryDocument(); - - disco.HttpResponse.StatusCode.Should().Be(HttpStatusCode.OK); - disco.IsError.Should().BeFalse($"{disco.Error} :: {disco.HttpErrorReason}"); - - var regEndpoint = disco.RegistrationEndpoint; - var reg = new Uri(regEndpoint!); - - var cert = Path.Combine(Path.Combine(AppContext.BaseDirectory, "CertStore/issued"), - "weatherApiClientLocalhostCert1.pfx"); - - var clientCert = new X509Certificate2(cert, "udap-test"); - var now = DateTime.UtcNow; - var jwtId = CryptoRandom.CreateUniqueId(); - - var document = new UdapDynamicClientRegistrationDocument - { - Issuer = "http://localhost/", - // Subject = "http://localhost/", - Audience = "https://localhost:5002/connect/register", - Expiration = EpochTime.GetIntDate(now.AddMinutes(1).ToUniversalTime()), - IssuedAt = EpochTime.GetIntDate(now.ToUniversalTime()), - JwtId = jwtId, - ClientName = "udapTestClient", - Contacts = new HashSet { "FhirJoe@BridgeTown.lab", "FhirJoe@test.lab" }, - GrantTypes = new HashSet { "client_credentials" }, - TokenEndpointAuthMethod = UdapConstants.RegistrationDocumentValues.TokenEndpointAuthMethodValue, - Scope = "system/Patient.* system/Practitioner.read" - }; - - document.Add("Extra", "Stuff" as string); - - var signedSoftwareStatement = - SignedSoftwareStatementBuilder - .Create(clientCert, document) - .Build(); - - var requestBody = new UdapRegisterRequest - ( - signedSoftwareStatement, - UdapConstants.UdapVersionsSupportedValue - ); - - var response = await client.PostAsJsonAsync(reg, requestBody); - - if (response.StatusCode != HttpStatusCode.Created) - { - _testOutputHelper.WriteLine(await response.Content.ReadAsStringAsync()); - } - - response.StatusCode.Should().Be(HttpStatusCode.BadRequest); - - var errorResponse = - await response.Content.ReadFromJsonAsync(); - - errorResponse.Should().NotBeNull(); - errorResponse!.Error.Should().Be(UdapDynamicClientRegistrationErrors.InvalidSoftwareStatement); - errorResponse.ErrorDescription.Should().Be(UdapDynamicClientRegistrationErrorDescriptions.SubIsMissing); - } - - - //invalid_software_statement - [Fact] - public async Task RegisrationInvalidSotwareStatement_subNotEqualtoIss_Test() - { - using var client = _fixture.CreateClient(); - var disco = await client.GetUdapDiscoveryDocument(); - - disco.HttpResponse.StatusCode.Should().Be(HttpStatusCode.OK); - disco.IsError.Should().BeFalse($"{disco.Error} :: {disco.HttpErrorReason}"); - - var regEndpoint = disco.RegistrationEndpoint; - var reg = new Uri(regEndpoint!); - - var cert = Path.Combine(Path.Combine(AppContext.BaseDirectory, "CertStore/issued"), - "weatherApiClientLocalhostCert1.pfx"); - - var clientCert = new X509Certificate2(cert, "udap-test"); - var now = DateTime.UtcNow; - var jwtId = CryptoRandom.CreateUniqueId(); - - var document = new UdapDynamicClientRegistrationDocument - { - Issuer = "http://localhost/", - Subject = "http://localhost:9999/", - Audience = "https://localhost:5002/connect/register", - Expiration = EpochTime.GetIntDate(now.AddMinutes(1).ToUniversalTime()), - IssuedAt = EpochTime.GetIntDate(now.ToUniversalTime()), - JwtId = jwtId, - ClientName = "udapTestClient", - Contacts = new HashSet { "FhirJoe@BridgeTown.lab", "FhirJoe@test.lab" }, - GrantTypes = new HashSet { "client_credentials" }, - TokenEndpointAuthMethod = UdapConstants.RegistrationDocumentValues.TokenEndpointAuthMethodValue, - Scope = "system/Patient.* system/Practitioner.read" - }; - - document.Add("Extra", "Stuff" as string); - - var signedSoftwareStatement = - SignedSoftwareStatementBuilder - .Create(clientCert, document) - .Build(); - - var requestBody = new UdapRegisterRequest - ( - signedSoftwareStatement, - UdapConstants.UdapVersionsSupportedValue - ); - - var response = await client.PostAsJsonAsync(reg, requestBody); - - if (response.StatusCode != HttpStatusCode.Created) - { - _testOutputHelper.WriteLine(await response.Content.ReadAsStringAsync()); - } - - response.StatusCode.Should().Be(HttpStatusCode.BadRequest); - - var errorResponse = - await response.Content.ReadFromJsonAsync(); - - errorResponse.Should().NotBeNull(); - errorResponse!.Error.Should().Be(UdapDynamicClientRegistrationErrors.InvalidSoftwareStatement); - errorResponse.ErrorDescription.Should().Be(UdapDynamicClientRegistrationErrorDescriptions.SubNotEqualToIss); - } - - //invalid_software_statement - [Fact] - public async Task RegisrationInvalidSotwareStatement_audMissing_Test() - { - using var client = _fixture.CreateClient(); - var disco = await client.GetUdapDiscoveryDocument(); - - disco.HttpResponse.StatusCode.Should().Be(HttpStatusCode.OK); - disco.IsError.Should().BeFalse($"{disco.Error} :: {disco.HttpErrorReason}"); - - var regEndpoint = disco.RegistrationEndpoint; - var reg = new Uri(regEndpoint!); - - var cert = Path.Combine(Path.Combine(AppContext.BaseDirectory, "CertStore/issued"), - "weatherApiClientLocalhostCert1.pfx"); - - var clientCert = new X509Certificate2(cert, "udap-test"); - var now = DateTime.UtcNow; - var jwtId = CryptoRandom.CreateUniqueId(); - - var document = new UdapDynamicClientRegistrationDocument - { - Issuer = "http://localhost/", - Subject = "http://localhost/", - // Audience = "https://localhost:5002/connect/register", - Expiration = EpochTime.GetIntDate(now.AddMinutes(1).ToUniversalTime()), - IssuedAt = EpochTime.GetIntDate(now.ToUniversalTime()), - JwtId = jwtId, - ClientName = "udapTestClient", - Contacts = new HashSet { "FhirJoe@BridgeTown.lab", "FhirJoe@test.lab" }, - GrantTypes = new HashSet { "client_credentials" }, - TokenEndpointAuthMethod = UdapConstants.RegistrationDocumentValues.TokenEndpointAuthMethodValue, - Scope = "system/Patient.* system/Practitioner.read" - }; - - document.Add("Extra", "Stuff" as string); - - var signedSoftwareStatement = - SignedSoftwareStatementBuilder - .Create(clientCert, document) - .Build(); - - var requestBody = new UdapRegisterRequest - ( - signedSoftwareStatement, - UdapConstants.UdapVersionsSupportedValue - ); - - var response = await client.PostAsJsonAsync(reg, requestBody); - - if (response.StatusCode != HttpStatusCode.Created) - { - _testOutputHelper.WriteLine(await response.Content.ReadAsStringAsync()); - } - - response.StatusCode.Should().Be(HttpStatusCode.BadRequest); - - var errorResponse = - await response.Content.ReadFromJsonAsync(); - - errorResponse.Should().NotBeNull(); - errorResponse!.Error.Should().Be(UdapDynamicClientRegistrationErrors.InvalidSoftwareStatement); - errorResponse.ErrorDescription.Should().Be($"{UdapDynamicClientRegistrationErrorDescriptions.InvalidAud}: "); - } - - //invalid_software_statement - [Fact] - public async Task RegisrationInvalidSotwareStatement_audEqualsRegistrationEndpoint_Test() - { - using var client = _fixture.CreateClient(); - var disco = await client.GetUdapDiscoveryDocument(); - - disco.HttpResponse.StatusCode.Should().Be(HttpStatusCode.OK); - disco.IsError.Should().BeFalse($"{disco.Error} :: {disco.HttpErrorReason}"); - - var regEndpoint = disco.RegistrationEndpoint; - var reg = new Uri(regEndpoint!); - - var cert = Path.Combine(Path.Combine(AppContext.BaseDirectory, "CertStore/issued"), - "weatherApiClientLocalhostCert1.pfx"); - - var clientCert = new X509Certificate2(cert, "udap-test"); - var now = DateTime.UtcNow; - var jwtId = CryptoRandom.CreateUniqueId(); - - var document = new UdapDynamicClientRegistrationDocument - { - Issuer = "http://localhost/", - Subject = "http://localhost/", - Audience = "https://localhost:5002/connect/register", - Expiration = EpochTime.GetIntDate(now.AddMinutes(1).ToUniversalTime()), - IssuedAt = EpochTime.GetIntDate(now.ToUniversalTime()), - JwtId = jwtId, - ClientName = "udapTestClient", - Contacts = new HashSet { "FhirJoe@BridgeTown.lab", "FhirJoe@test.lab" }, - GrantTypes = new HashSet { "client_credentials" }, - TokenEndpointAuthMethod = UdapConstants.RegistrationDocumentValues.TokenEndpointAuthMethodValue, - Scope = "system/Patient.* system/Practitioner.read" - }; - - document.Add("Extra", "Stuff" as string); - - var signedSoftwareStatement = - SignedSoftwareStatementBuilder - .Create(clientCert, document) - .Build(); - - var requestBody = new UdapRegisterRequest - ( - signedSoftwareStatement, - UdapConstants.UdapVersionsSupportedValue - ); - - var response = await client.PostAsJsonAsync(reg, requestBody); - - if (response.StatusCode != HttpStatusCode.Created) - { - _testOutputHelper.WriteLine(await response.Content.ReadAsStringAsync()); - } - - response.StatusCode.Should().Be(HttpStatusCode.BadRequest); - - var errorResponse = - await response.Content.ReadFromJsonAsync(); - - errorResponse.Should().NotBeNull(); - errorResponse!.Error.Should().Be(UdapDynamicClientRegistrationErrors.InvalidSoftwareStatement); - errorResponse.ErrorDescription.Should().Be($"{UdapDynamicClientRegistrationErrorDescriptions.InvalidMatchAud}"); - } - - //invalid_software_statement - [Fact] - public async Task RegisrationInvalidSotwareStatement_expMissing_Test() - { - using var client = _fixture.CreateClient(); - var disco = await client.GetUdapDiscoveryDocument(); - - disco.HttpResponse.StatusCode.Should().Be(HttpStatusCode.OK); - disco.IsError.Should().BeFalse($"{disco.Error} :: {disco.HttpErrorReason}"); - - var regEndpoint = disco.RegistrationEndpoint; - var reg = new Uri(regEndpoint!); - - var cert = Path.Combine(Path.Combine(AppContext.BaseDirectory, "CertStore/issued"), - "weatherApiClientLocalhostCert1.pfx"); - - var clientCert = new X509Certificate2(cert, "udap-test"); - var now = DateTime.UtcNow; - var jwtId = CryptoRandom.CreateUniqueId(); - - var document = new UdapDynamicClientRegistrationDocument - { - Issuer = "http://localhost/", - Subject = "http://localhost/", - Audience = "https://localhost:5002/connect/register", - // Expiration = EpochTime.GetIntDate(now.AddMinutes(1).ToUniversalTime()), - IssuedAt = EpochTime.GetIntDate(now.ToUniversalTime()), - JwtId = jwtId, - ClientName = "udapTestClient", - Contacts = new HashSet { "FhirJoe@BridgeTown.lab", "FhirJoe@test.lab" }, - GrantTypes = new HashSet { "client_credentials" }, - TokenEndpointAuthMethod = UdapConstants.RegistrationDocumentValues.TokenEndpointAuthMethodValue, - Scope = "system/Patient.* system/Practitioner.read" - }; - - document.Add("Extra", "Stuff" as string); - - var signedSoftwareStatement = - SignedSoftwareStatementBuilder - .Create(clientCert, document) - .Build(); - - var requestBody = new UdapRegisterRequest - ( - signedSoftwareStatement, - UdapConstants.UdapVersionsSupportedValue - ); - - var response = await client.PostAsJsonAsync(reg, requestBody); - - if (response.StatusCode != HttpStatusCode.Created) - { - _testOutputHelper.WriteLine(await response.Content.ReadAsStringAsync()); - } - - response.StatusCode.Should().Be(HttpStatusCode.BadRequest); - - var errorResponse = - await response.Content.ReadFromJsonAsync(); - - errorResponse.Should().NotBeNull(); - errorResponse!.Error.Should().Be(UdapDynamicClientRegistrationErrors.InvalidSoftwareStatement); - errorResponse.ErrorDescription.Should().Be($"{UdapDynamicClientRegistrationErrorDescriptions.ExpMissing}"); - } - - //invalid_software_statement - [Fact] - public async Task RegisrationInvalidSotwareStatement_expExpired_Test() - { - using var client = _fixture.CreateClient(); - var disco = await client.GetUdapDiscoveryDocument(); - - disco.HttpResponse.StatusCode.Should().Be(HttpStatusCode.OK); - disco.IsError.Should().BeFalse($"{disco.Error} :: {disco.HttpErrorReason}"); - - var regEndpoint = disco.RegistrationEndpoint; - var reg = new Uri(regEndpoint!); - - var cert = Path.Combine(Path.Combine(AppContext.BaseDirectory, "CertStore/issued"), - "weatherApiClientLocalhostCert1.pfx"); - - var clientCert = new X509Certificate2(cert, "udap-test"); - var now = DateTime.UtcNow; - var jwtId = CryptoRandom.CreateUniqueId(); - - var document = new UdapDynamicClientRegistrationDocument - { - Issuer = "http://localhost/", - Subject = "http://localhost/", - Audience = "https://localhost:5002/connect/register", - Expiration = EpochTime.GetIntDate(now.AddMinutes(-5).ToUniversalTime()), - IssuedAt = EpochTime.GetIntDate(now.ToUniversalTime()), - JwtId = jwtId, - ClientName = "udapTestClient", - Contacts = new HashSet { "FhirJoe@BridgeTown.lab", "FhirJoe@test.lab" }, - GrantTypes = new HashSet { "client_credentials" }, - TokenEndpointAuthMethod = UdapConstants.RegistrationDocumentValues.TokenEndpointAuthMethodValue, - Scope = "system/Patient.* system/Practitioner.read" - }; - - document.Add("Extra", "Stuff" as string); - - var signedSoftwareStatement = - SignedSoftwareStatementBuilder - .Create(clientCert, document) - .Build(); - - var requestBody = new UdapRegisterRequest - ( - signedSoftwareStatement, - UdapConstants.UdapVersionsSupportedValue - ); - - var response = await client.PostAsJsonAsync(reg, requestBody); - - if (response.StatusCode != HttpStatusCode.Created) - { - _testOutputHelper.WriteLine(await response.Content.ReadAsStringAsync()); - } - - response.StatusCode.Should().Be(HttpStatusCode.BadRequest); - - var errorResponse = - await response.Content.ReadFromJsonAsync(); - - errorResponse.Should().NotBeNull(); - errorResponse!.Error.Should().Be(UdapDynamicClientRegistrationErrors.InvalidSoftwareStatement); - errorResponse.ErrorDescription.Should().Contain($"{UdapDynamicClientRegistrationErrorDescriptions.ExpExpired}"); - } - - //invalid_software_statement - [Fact] - public async Task RegisrationInvalidSotwareStatement_iatMissing_Test() - { - using var client = _fixture.CreateClient(); - var disco = await client.GetUdapDiscoveryDocument(); - - disco.HttpResponse.StatusCode.Should().Be(HttpStatusCode.OK); - disco.IsError.Should().BeFalse($"{disco.Error} :: {disco.HttpErrorReason}"); - - var regEndpoint = disco.RegistrationEndpoint; - var reg = new Uri(regEndpoint!); - - var cert = Path.Combine(Path.Combine(AppContext.BaseDirectory, "CertStore/issued"), - "weatherApiClientLocalhostCert1.pfx"); - - var clientCert = new X509Certificate2(cert, "udap-test"); - var now = DateTime.UtcNow; - var jwtId = CryptoRandom.CreateUniqueId(); - - var document = new UdapDynamicClientRegistrationDocument - { - Issuer = "http://localhost/", - Subject = "http://localhost/", - Audience = "https://localhost/connect/register", - Expiration = EpochTime.GetIntDate(now.AddMinutes(1).ToUniversalTime()), - //IssuedAt = EpochTime.GetIntDate(now.ToUniversalTime()), - JwtId = jwtId, - ClientName = "udapTestClient", - Contacts = new HashSet { "FhirJoe@BridgeTown.lab", "FhirJoe@test.lab" }, - GrantTypes = new HashSet { "client_credentials" }, - TokenEndpointAuthMethod = UdapConstants.RegistrationDocumentValues.TokenEndpointAuthMethodValue, - Scope = "system/Patient.* system/Practitioner.read" - }; - - document.Add("Extra", "Stuff" as string); - - var signedSoftwareStatement = - SignedSoftwareStatementBuilder - .Create(clientCert, document) - .Build(); - - var requestBody = new UdapRegisterRequest - ( - signedSoftwareStatement, - UdapConstants.UdapVersionsSupportedValue - ); - - var response = await client.PostAsJsonAsync(reg, requestBody); - - if (response.StatusCode != HttpStatusCode.Created) - { - _testOutputHelper.WriteLine(await response.Content.ReadAsStringAsync()); - } - - response.StatusCode.Should().Be(HttpStatusCode.BadRequest); - - var errorResponse = - await response.Content.ReadFromJsonAsync(); - - errorResponse.Should().NotBeNull(); - errorResponse!.Error.Should().Be(UdapDynamicClientRegistrationErrors.InvalidSoftwareStatement); - errorResponse.ErrorDescription.Should().Be($"{UdapDynamicClientRegistrationErrorDescriptions.IssuedAtMissing}"); - } - - //invalid_client_metadata - [Fact] - public async Task RegistrationInvalidClientMetadata_clientName_Missing_Test() - { - using var client = _fixture.CreateClient(); - var disco = await client.GetUdapDiscoveryDocument(); - - disco.HttpResponse.StatusCode.Should().Be(HttpStatusCode.OK); - disco.IsError.Should().BeFalse($"{disco.Error} :: {disco.HttpErrorReason}"); - - var regEndpoint = disco.RegistrationEndpoint; - var reg = new Uri(regEndpoint!); - - var cert = Path.Combine(Path.Combine(AppContext.BaseDirectory, "CertStore/issued"), - "weatherApiClientLocalhostCert1.pfx"); - - var clientCert = new X509Certificate2(cert, "udap-test"); - var now = DateTime.UtcNow; - var jwtId = CryptoRandom.CreateUniqueId(); - - var document = new UdapDynamicClientRegistrationDocument - { - Issuer = "http://localhost/", - Subject = "http://localhost/", - Audience = "https://localhost/connect/register", - Expiration = EpochTime.GetIntDate(now.AddMinutes(1).ToUniversalTime()), - IssuedAt = EpochTime.GetIntDate(now.ToUniversalTime()), - JwtId = jwtId, - // ClientName = "udapTestClient", - Contacts = new HashSet { "FhirJoe@BridgeTown.lab", "FhirJoe@test.lab" }, - GrantTypes = new HashSet { "client_credentials" }, - TokenEndpointAuthMethod = UdapConstants.RegistrationDocumentValues.TokenEndpointAuthMethodValue, - Scope = "system/Patient.* system/Practitioner.read" - }; - - document.Add("Extra", "Stuff" as string); - - var signedSoftwareStatement = - SignedSoftwareStatementBuilder - .Create(clientCert, document) - .Build(); - - var requestBody = new UdapRegisterRequest - ( - signedSoftwareStatement, - UdapConstants.UdapVersionsSupportedValue - ); - - var response = await client.PostAsJsonAsync(reg, requestBody); - - if (response.StatusCode != HttpStatusCode.Created) - { - _testOutputHelper.WriteLine(await response.Content.ReadAsStringAsync()); - } - - response.StatusCode.Should().Be(HttpStatusCode.BadRequest); - - var errorResponse = - await response.Content.ReadFromJsonAsync(); - - errorResponse.Should().NotBeNull(); - errorResponse!.Error.Should().Be(UdapDynamicClientRegistrationErrors.InvalidClientMetadata); - errorResponse.ErrorDescription.Should().Be($"{UdapDynamicClientRegistrationErrorDescriptions.ClientNameMissing}"); - } - - //invalid_client_metadata - // - // Remember and empty grant_types is a cancel registration - // http://hl7.org/fhir/us/udap-security/registration.html#modifying-and-cancelling-registrations - // But a missing grant_types is an error - // - [Fact] - public async Task RegisrationInvalidClientMetadata_grant_types_Missing_Test() - { - using var client = _fixture.CreateClient(); - var disco = await client.GetUdapDiscoveryDocument(); - - disco.HttpResponse.StatusCode.Should().Be(HttpStatusCode.OK); - disco.IsError.Should().BeFalse($"{disco.Error} :: {disco.HttpErrorReason}"); - - var regEndpoint = disco.RegistrationEndpoint; - var reg = new Uri(regEndpoint!); - - var cert = Path.Combine(Path.Combine(AppContext.BaseDirectory, "CertStore/issued"), - "weatherApiClientLocalhostCert1.pfx"); - - var clientCert = new X509Certificate2(cert, "udap-test"); - var now = DateTime.UtcNow; - var jwtId = CryptoRandom.CreateUniqueId(); - - var document = new UdapDynamicClientRegistrationDocument - { - Issuer = "http://localhost/", - Subject = "http://localhost/", - Audience = "https://localhost/connect/register", - Expiration = EpochTime.GetIntDate(now.AddMinutes(1).ToUniversalTime()), - IssuedAt = EpochTime.GetIntDate(now.ToUniversalTime()), - JwtId = jwtId, - ClientName = "udapTestClient", - Contacts = new HashSet { "FhirJoe@BridgeTown.lab", "FhirJoe@test.lab" }, - // GrantTypes = new HashSet { "client_credentials" }, - TokenEndpointAuthMethod = UdapConstants.RegistrationDocumentValues.TokenEndpointAuthMethodValue, - Scope = "system/Patient.* system/Practitioner.read" - }; - - document.Add("Extra", "Stuff" as string); - - var signedSoftwareStatement = - SignedSoftwareStatementBuilder - .Create(clientCert, document) - .Build(); - - var requestBody = new UdapRegisterRequest - ( - signedSoftwareStatement, - UdapConstants.UdapVersionsSupportedValue - ); - - var response = await client.PostAsJsonAsync(reg, requestBody); - - if (response.StatusCode != HttpStatusCode.Created) - { - _testOutputHelper.WriteLine(await response.Content.ReadAsStringAsync()); - } - - response.StatusCode.Should().Be(HttpStatusCode.BadRequest); - - var errorResponse = - await response.Content.ReadFromJsonAsync(); - - errorResponse.Should().NotBeNull(); - errorResponse!.Error.Should().Be(UdapDynamicClientRegistrationErrors.InvalidClientMetadata); - errorResponse.ErrorDescription.Should().Be($"{UdapDynamicClientRegistrationErrorDescriptions.GrantTypeMissing}"); - } - - //invalid_client_metadata - [Fact] - public async Task RegisrationInvalidClientMetadata_responseTypes_Missing_Test() - { - using var client = _fixture.CreateClient(); - var disco = await client.GetUdapDiscoveryDocument(); - - disco.HttpResponse.StatusCode.Should().Be(HttpStatusCode.OK); - disco.IsError.Should().BeFalse($"{disco.Error} :: {disco.HttpErrorReason}"); - - var regEndpoint = disco.RegistrationEndpoint; - var reg = new Uri(regEndpoint!); - - var cert = Path.Combine(Path.Combine(AppContext.BaseDirectory, "CertStore/issued"), - "weatherApiClientLocalhostCert1.pfx"); - - var clientCert = new X509Certificate2(cert, "udap-test"); - var now = DateTime.UtcNow; - var jwtId = CryptoRandom.CreateUniqueId(); - - var document = new UdapDynamicClientRegistrationDocument - { - Issuer = "http://localhost/", - Subject = "http://localhost/", - Audience = "https://localhost/connect/register", - Expiration = EpochTime.GetIntDate(now.AddMinutes(1).ToUniversalTime()), - IssuedAt = EpochTime.GetIntDate(now.ToUniversalTime()), - JwtId = jwtId, - ClientName = "udapTestClient", - Contacts = new HashSet { "FhirJoe@BridgeTown.lab", "FhirJoe@test.lab" }, - GrantTypes = new HashSet { "authorization_code" }, - TokenEndpointAuthMethod = UdapConstants.RegistrationDocumentValues.TokenEndpointAuthMethodValue, - Scope = "user/Patient.* user/Practitioner.read", - RedirectUris = new List { new Uri($"https://client.fhirlabs.net/redirect/{Guid.NewGuid()}").AbsoluteUri }, - }; - - document.Add("Extra", "Stuff" as string); - - var signedSoftwareStatement = - SignedSoftwareStatementBuilder - .Create(clientCert, document) - .Build(); - - var requestBody = new UdapRegisterRequest - ( - signedSoftwareStatement, - UdapConstants.UdapVersionsSupportedValue - ); - - var response = await client.PostAsJsonAsync(reg, requestBody); - - if (response.StatusCode != HttpStatusCode.Created) - { - _testOutputHelper.WriteLine(await response.Content.ReadAsStringAsync()); - } - - response.StatusCode.Should().Be(HttpStatusCode.BadRequest); - - var errorResponse = - await response.Content.ReadFromJsonAsync(); - - errorResponse.Should().NotBeNull(); - errorResponse!.Error.Should().Be(UdapDynamicClientRegistrationErrors.InvalidClientMetadata); - errorResponse.ErrorDescription.Should().Be($"{UdapDynamicClientRegistrationErrorDescriptions.ResponseTypesMissing}"); - } - - //invalid_client_metadata - [Fact] - public async Task RegisrationInvalidClientMetadata_tokenEndpointAuthMethodMissing_Test() - { - using var client = _fixture.CreateClient(); - var disco = await client.GetUdapDiscoveryDocument(); - - disco.HttpResponse.StatusCode.Should().Be(HttpStatusCode.OK); - disco.IsError.Should().BeFalse($"{disco.Error} :: {disco.HttpErrorReason}"); - - var regEndpoint = disco.RegistrationEndpoint; - var reg = new Uri(regEndpoint!); - - var cert = Path.Combine(Path.Combine(AppContext.BaseDirectory, "CertStore/issued"), - "weatherApiClientLocalhostCert1.pfx"); - - var clientCert = new X509Certificate2(cert, "udap-test"); - var now = DateTime.UtcNow; - var jwtId = CryptoRandom.CreateUniqueId(); - - var document = new UdapDynamicClientRegistrationDocument - { - Issuer = "http://localhost/", - Subject = "http://localhost/", - Audience = "https://localhost/connect/register", - Expiration = EpochTime.GetIntDate(now.AddMinutes(1).ToUniversalTime()), - IssuedAt = EpochTime.GetIntDate(now.ToUniversalTime()), - JwtId = jwtId, - ClientName = "udapTestClient", - Contacts = new HashSet { "FhirJoe@BridgeTown.lab", "FhirJoe@test.lab" }, - GrantTypes = new HashSet { "client_credentials" }, - //TokenEndpointAuthMethod = UdapConstants.RegistrationDocumentValues.TokenEndpointAuthMethodValue, - Scope = "system/Patient.* system/Practitioner.read" - }; - - document.Add("Extra", "Stuff" as string); - - var signedSoftwareStatement = - SignedSoftwareStatementBuilder - .Create(clientCert, document) - .Build(); - - var requestBody = new UdapRegisterRequest - ( - signedSoftwareStatement, - UdapConstants.UdapVersionsSupportedValue - ); - - var response = await client.PostAsJsonAsync(reg, requestBody); - - if (response.StatusCode != HttpStatusCode.Created) - { - _testOutputHelper.WriteLine(await response.Content.ReadAsStringAsync()); - } - - response.StatusCode.Should().Be(HttpStatusCode.BadRequest); - - var errorResponse = - await response.Content.ReadFromJsonAsync(); - - errorResponse.Should().NotBeNull(); - errorResponse!.Error.Should().Be(UdapDynamicClientRegistrationErrors.InvalidClientMetadata); - errorResponse.ErrorDescription.Should().Be($"{UdapDynamicClientRegistrationErrorDescriptions.TokenEndpointAuthMethodMissing}"); - } - - private async Task ResetClientInDatabase() - { - foreach (var dbClient in _fixture.UdapDbAdminContext.Clients) - { - _fixture.UdapDbAdminContext.Clients.Remove(dbClient); - } - - await _fixture.UdapDbAdminContext.SaveChangesAsync(); - } -} \ No newline at end of file diff --git a/examples/FhirLabsApi/FhirLabsApi.csproj b/examples/FhirLabsApi/FhirLabsApi.csproj index 0e235876..e16011e6 100644 --- a/examples/FhirLabsApi/FhirLabsApi.csproj +++ b/examples/FhirLabsApi/FhirLabsApi.csproj @@ -45,9 +45,9 @@ - - - + + + diff --git a/examples/Udap.Auth.Server.Admin/Properties/launchSettings.json b/examples/Udap.Auth.Server.Admin/Properties/launchSettings.json index 9b9f550e..a8677490 100644 --- a/examples/Udap.Auth.Server.Admin/Properties/launchSettings.json +++ b/examples/Udap.Auth.Server.Admin/Properties/launchSettings.json @@ -18,16 +18,7 @@ }, "dotnetRunMessages": true, "applicationUrl": "http://localhost:5253" - }, - "SecuredControls": { - "commandName": "Project", - "launchBrowser": true, - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - }, - "sslPort": 5002, - "applicationUrl": "https://admin.securedcontrols.net:5002" - }, + }, "/seed": { "commandName": "Project", "commandLineArgs": "/seed ../../../../../_tests/Udap.PKI.Generator/certstores", diff --git a/examples/Udap.Auth.Server.Admin/SeedData.cs b/examples/Udap.Auth.Server.Admin/SeedData.cs index 719515e4..b8bf579d 100644 --- a/examples/Udap.Auth.Server.Admin/SeedData.cs +++ b/examples/Udap.Auth.Server.Admin/SeedData.cs @@ -168,7 +168,6 @@ public static void EnsureSeedData(string connectionString, string certStoreBaseP } var apiScopes = configDbContext.ApiScopes - .Where(s => s.Enabled) .Select(s => s.Name) .ToList(); diff --git a/examples/Udap.Auth.Server.Admin/Udap.Auth.Server.Admin.csproj b/examples/Udap.Auth.Server.Admin/Udap.Auth.Server.Admin.csproj index a8cd4981..e627755a 100644 --- a/examples/Udap.Auth.Server.Admin/Udap.Auth.Server.Admin.csproj +++ b/examples/Udap.Auth.Server.Admin/Udap.Auth.Server.Admin.csproj @@ -12,17 +12,17 @@ - - - - + + + + - - + + - + diff --git a/examples/Udap.Auth.Server/HostingExtensions.cs b/examples/Udap.Auth.Server/HostingExtensions.cs index 08108087..bfd05299 100644 --- a/examples/Udap.Auth.Server/HostingExtensions.cs +++ b/examples/Udap.Auth.Server/HostingExtensions.cs @@ -71,7 +71,6 @@ public static WebApplication ConfigureServices(this WebApplicationBuilder builde var udapServerOptions = builder.Configuration.GetOption("ServerSettings"); options.DefaultSystemScopes = udapServerOptions.DefaultSystemScopes; options.DefaultUserScopes = udapServerOptions.DefaultUserScopes; - options.ServerSupport = udapServerOptions.ServerSupport; options.ForceStateParamOnAuthorizationCode = udapServerOptions.ForceStateParamOnAuthorizationCode; options.LogoRequired = udapServerOptions.LogoRequired; options.RequireConsent = udapServerOptions.RequireConsent; diff --git a/examples/Udap.Auth.Server/Properties/launchSettings.json b/examples/Udap.Auth.Server/Properties/launchSettings.json index 01984f75..1aa5bc78 100644 --- a/examples/Udap.Auth.Server/Properties/launchSettings.json +++ b/examples/Udap.Auth.Server/Properties/launchSettings.json @@ -17,7 +17,6 @@ "environmentVariables": { "GCPDeploy": "false", "ASPNETCORE_ENVIRONMENT": "Development", - "ServerSettings:ServerSupport": "UDAP", "ServerSettings:DefaultSystemScopes": "udap system.cruds system/*.rs", "ServerSettings:DefaultUserScopes": "udap user.cruds", "ServerSettings:ForceStateParamOnAuthorizationCode": "true", @@ -32,7 +31,6 @@ "environmentVariables": { "GCPDeploy": "true", "ASPNETCORE_ENVIRONMENT": "Development", - "ServerSettings:ServerSupport": "UDAP", "ServerSettings:DefaultSystemScopes": "udap system.cruds", "ServerSettings:DefaultUserScopes": "udap user.cruds", "UdapIdpBaseUrl": "https://host.docker.internal:5002" diff --git a/examples/Udap.Auth.Server/Udap.Auth.Server.csproj b/examples/Udap.Auth.Server/Udap.Auth.Server.csproj index c7b08eb7..c05b4684 100644 --- a/examples/Udap.Auth.Server/Udap.Auth.Server.csproj +++ b/examples/Udap.Auth.Server/Udap.Auth.Server.csproj @@ -18,20 +18,20 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all - - + + - - - - - - - + + + + + + + diff --git a/examples/Udap.Auth.Server/appsettings.Development.json b/examples/Udap.Auth.Server/appsettings.Development.json index 89948884..7e98e96d 100644 --- a/examples/Udap.Auth.Server/appsettings.Development.json +++ b/examples/Udap.Auth.Server/appsettings.Development.json @@ -22,7 +22,6 @@ }, "ServerSettings": { - "ServerSupport": "Hl7SecurityIG", "LogoRequired": "true" }, diff --git a/examples/Udap.Auth.Server/appsettings.Production.json b/examples/Udap.Auth.Server/appsettings.Production.json index b243ff0e..e04fbe50 100644 --- a/examples/Udap.Auth.Server/appsettings.Production.json +++ b/examples/Udap.Auth.Server/appsettings.Production.json @@ -22,15 +22,14 @@ "ServerSettings": { - "ServerSupport": "UDAP", "LogoRequired": "true", //https://hl7.org/fhir/smart-app-launch/scopes-and-launch-context.html "DefaultSystemScopes": "openid system/*.rs system/*.read", "DefaultUserScopes": "openid user/*.rs user/*/read", - "ForceStateParamOnAuthorizationCode": true + "ForceStateParamOnAuthorizationCode": true }, "ConnectionStrings": { - "DefaultConnection": "Data Source=host.docker.internal;Initial Catalog=Udap.Idp.db;User ID=udap_user;Password=udap_password1;TrustServerCertificate=True;" + "DefaultConnection": "Host=host.docker.internal;Port=5432;Database=Udap.Auth.db;Username=udap_user;Password=udap_password1" } } \ No newline at end of file diff --git a/examples/Udap.Auth.Server/cloudbuild.yaml b/examples/Udap.Auth.Server/cloudbuild.yaml index c391d111..a0019538 100644 --- a/examples/Udap.Auth.Server/cloudbuild.yaml +++ b/examples/Udap.Auth.Server/cloudbuild.yaml @@ -17,7 +17,7 @@ steps: '--image', 'us-west1-docker.pkg.dev/$PROJECT_ID/cloud-run-source-deploy/udap.auth.server:$TAG_NAME', '--max-instances', '1', '--concurrency', '5', - '--set-env-vars', 'GCLOUD_PROJECT=true,GCPDeploy=true,UdapIdpBaseUrl=https://securedcontrols.net,DOTNET_SYSTEM_NET_HTTP_SOCKETSHTTPHANDLER_HTTP3SUPPORT=false', + '--set-env-vars', 'GCLOUD_PROJECT=true,GCPDeploy=true,UdapIdpBaseUrl=https://securedcontrols.net,DOTNET_SYSTEM_NET_HTTP_SOCKETSHTTPHANDLER_HTTP3SUPPORT=false,proxy-hosts=35.212.149.197 idp1.securedcontrols.net;35.212.149.197 idp2.securedcontrols.net;35.212.149.197 udap.zimt.work', '--vpc-connector', 'alloydb-connector', '--vpc-egress', 'all-traffic', '--ingress', 'internal-and-cloud-load-balancing', diff --git a/examples/Udap.CA/Udap.CA.csproj b/examples/Udap.CA/Udap.CA.csproj index a0b44403..699f89e0 100644 --- a/examples/Udap.CA/Udap.CA.csproj +++ b/examples/Udap.CA/Udap.CA.csproj @@ -11,14 +11,14 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/examples/Udap.Identity.Provider.2/Udap.Identity.Provider.2.csproj b/examples/Udap.Identity.Provider.2/Udap.Identity.Provider.2.csproj index 6901cf3a..06226cc8 100644 --- a/examples/Udap.Identity.Provider.2/Udap.Identity.Provider.2.csproj +++ b/examples/Udap.Identity.Provider.2/Udap.Identity.Provider.2.csproj @@ -21,20 +21,20 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all - - + + - - - - - - - + + + + + + + diff --git a/examples/Udap.Identity.Provider.2/appsettings.Development.json b/examples/Udap.Identity.Provider.2/appsettings.Development.json index 7b65143a..eed4d293 100644 --- a/examples/Udap.Identity.Provider.2/appsettings.Development.json +++ b/examples/Udap.Identity.Provider.2/appsettings.Development.json @@ -7,7 +7,6 @@ }, "ServerSettings": { - "ServerSupport": "UDAP", "DefaultUserScopes": "openid udap fhirUser email profile", "ForceStateParamOnAuthorizationCode": true }, diff --git a/examples/Udap.Identity.Provider.2/appsettings.Production.json b/examples/Udap.Identity.Provider.2/appsettings.Production.json index de57cf40..d31ca33c 100644 --- a/examples/Udap.Identity.Provider.2/appsettings.Production.json +++ b/examples/Udap.Identity.Provider.2/appsettings.Production.json @@ -1,6 +1,5 @@ { "ServerSettings": { - "ServerSupport": "UDAP", "DefaultUserScopes": "openid fhirUser email profile", "ForceStateParamOnAuthorizationCode": true }, diff --git a/examples/Udap.Identity.Provider/HostingExtensions.cs b/examples/Udap.Identity.Provider/HostingExtensions.cs index 3581f21c..261227f7 100644 --- a/examples/Udap.Identity.Provider/HostingExtensions.cs +++ b/examples/Udap.Identity.Provider/HostingExtensions.cs @@ -51,7 +51,6 @@ public static WebApplication ConfigureServices(this WebApplicationBuilder builde var udapServerOptions = builder.Configuration.GetOption("ServerSettings"); options.DefaultSystemScopes = udapServerOptions.DefaultSystemScopes; options.DefaultUserScopes = udapServerOptions.DefaultUserScopes; - options.ServerSupport = udapServerOptions.ServerSupport; options.ForceStateParamOnAuthorizationCode = udapServerOptions.ForceStateParamOnAuthorizationCode; options.LogoRequired = udapServerOptions.LogoRequired; options.AlwaysIncludeUserClaimsInIdToken = udapServerOptions.AlwaysIncludeUserClaimsInIdToken; diff --git a/examples/Udap.Identity.Provider/Udap.Identity.Provider.csproj b/examples/Udap.Identity.Provider/Udap.Identity.Provider.csproj index d96e6578..0149f822 100644 --- a/examples/Udap.Identity.Provider/Udap.Identity.Provider.csproj +++ b/examples/Udap.Identity.Provider/Udap.Identity.Provider.csproj @@ -21,20 +21,20 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all - - + + - - - - - - - + + + + + + + diff --git a/examples/Udap.Identity.Provider/appsettings.Development.json b/examples/Udap.Identity.Provider/appsettings.Development.json index 893b638a..bf880c3a 100644 --- a/examples/Udap.Identity.Provider/appsettings.Development.json +++ b/examples/Udap.Identity.Provider/appsettings.Development.json @@ -11,7 +11,6 @@ }, "ServerSettings": { - "ServerSupport": "UDAP", "DefaultUserScopes": "openid udap fhirUser email profile", "ForceStateParamOnAuthorizationCode": true, "AlwaysIncludeUserClaimsInIdToken": true diff --git a/examples/Udap.Identity.Provider/appsettings.Production.json b/examples/Udap.Identity.Provider/appsettings.Production.json index 71193336..babcf513 100644 --- a/examples/Udap.Identity.Provider/appsettings.Production.json +++ b/examples/Udap.Identity.Provider/appsettings.Production.json @@ -1,6 +1,5 @@ { "ServerSettings": { - "ServerSupport": "UDAP", "LogoRequired": "false", "DefaultUserScopes": "openid fhirUser email profile", "ForceStateParamOnAuthorizationCode": true, diff --git a/examples/Udap.Proxy.Server/Program.cs b/examples/Udap.Proxy.Server/Program.cs index a7da6989..574b0206 100644 --- a/examples/Udap.Proxy.Server/Program.cs +++ b/examples/Udap.Proxy.Server/Program.cs @@ -7,6 +7,18 @@ using Yarp.ReverseProxy.Transforms; using Google.Apis.Auth.OAuth2; using Udap.Smart.Model; +using Hl7.Fhir.Rest; +using Hl7.Fhir.Serialization; +using Hl7.Fhir.Model; +using System; +using Microsoft.IdentityModel.JsonWebTokens; +using ZiggyCreatures.Caching.Fusion; +using Task = System.Threading.Tasks.Task; +using System.IdentityModel.Tokens.Jwt; +using Microsoft.AspNetCore.Mvc; +using Udap.Metadata.Server; +using Udap.Model; +using Udap.Util.Extensions; var builder = WebApplication.CreateBuilder(args); @@ -19,6 +31,14 @@ builder.Services.Configure(builder.Configuration.GetRequiredSection("SmartMetadata")); builder.Services.AddSmartMetadata(); builder.Services.AddUdapMetadataServer(builder.Configuration); +builder.Services.AddFusionCache() + .WithDefaultEntryOptions(new FusionCacheEntryOptions + { + Duration = TimeSpan.FromMinutes(10), + FactorySoftTimeout = TimeSpan.FromMilliseconds(100), + AllowTimedOutFactoryBackgroundCompletion = true, + FailSafeMaxDuration = TimeSpan.FromHours(12) + }); builder.Services.AddAuthentication(OidcConstants.AuthenticationSchemes.AuthorizationHeaderBearer) @@ -26,7 +46,7 @@ { options.Authority = builder.Configuration["Jwt:Authority"]; options.RequireHttpsMetadata = bool.Parse(builder.Configuration["Jwt:RequireHttpsMetadata"] ?? "true"); - + options.TokenValidationParameters = new TokenValidationParameters { ValidateAudience = false @@ -45,37 +65,69 @@ .ConfigureHttpClient((context, handler) => { // this is required to decompress automatically. ******* troubleshooting only ******* - handler.AutomaticDecompression = System.Net.DecompressionMethods.All; + handler.AutomaticDecompression = System.Net.DecompressionMethods.All; }) .AddTransforms(builderContext => { // Conditionally add a transform for routes that require auth. - if (builderContext.Route.Metadata != null && + if (builderContext.Route.Metadata != null && (builderContext.Route.Metadata.ContainsKey("GCPKeyResolve") || builderContext.Route.Metadata.ContainsKey("AccessToken"))) { builderContext.AddRequestTransform(async context => { - context.ProxyRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await ResolveAccessToken(builderContext.Route.Metadata)); - - // Google Cloud way of passing scopes to the Fhir Server - // context.ProxyRequest.Headers.Add("X-Authorization-Scope", "user/Patient.read launch/patient"); - // context.ProxyRequest.Headers.Add("X-Authorization-Issuer", "securedcontrols.net"); + var resolveAccessToken = await ResolveAccessToken(builderContext.Route.Metadata); + context.ProxyRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", resolveAccessToken); + + SetProxyHeaders(context); }); } // Use the default credentials. Primary usage: running in Cloud Run under a specific service account if (builderContext.Route.Metadata != null && (builderContext.Route.Metadata.TryGetValue("ADC", out string? adc))) { - if(adc == "True") + if (adc.Equals("True", StringComparison.OrdinalIgnoreCase)) { builderContext.AddRequestTransform(async context => { var googleCredentials = GoogleCredential.GetApplicationDefault(); string accessToken = await googleCredentials.UnderlyingCredential.GetAccessTokenForRequestAsync(); context.ProxyRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); + + SetProxyHeaders(context); }); } } + + builderContext.AddResponseTransform(async responseContext => + { + if (responseContext.HttpContext.Request.Path == "/fhir/r4/metadata") + { + responseContext.SuppressResponseBody = true; + var cache = responseContext.HttpContext.RequestServices.GetRequiredService(); + var bytes = await cache.GetOrSetAsync("metadata", _ => GetFhirMetadata(responseContext, builder)); + + // Change Content-Length to match the modified body, or remove it. + responseContext.HttpContext.Response.ContentLength = bytes?.Length; + + // Response headers are copied before transforms are invoked, update any needed headers on the HttpContext.Response. + await responseContext.HttpContext.Response.Body.WriteAsync(bytes); + } + else if (responseContext.HttpContext.Request.Path.HasValue && + responseContext.HttpContext.Request.Path.Value.StartsWith("/fhir/r4/", StringComparison.OrdinalIgnoreCase)) + { + responseContext.SuppressResponseBody = true; + var stream = await responseContext.ProxyResponse!.Content.ReadAsStreamAsync(); + using var reader = new StreamReader(stream); + // TODO: size limits, timeouts + var body = await reader.ReadToEndAsync(); + + var finalBytes = Encoding.UTF8.GetBytes(body.Replace($"\"url\": \"{builder.Configuration["FhirUrlProxy:Back"]}", + $"\"url\": \"{builder.Configuration["FhirUrlProxy:Front"]}")); + responseContext.HttpContext.Response.ContentLength = finalBytes.Length; + + await responseContext.HttpContext.Response.Body.WriteAsync(finalBytes); + } + }); }); var app = builder.Build(); @@ -92,7 +144,7 @@ app.MapReverseProxy(); app.UseSmartMetadata(); -app.UseUdapMetadataServer(); +app.UseUdapMetadataServer("fhir/r4"); // Ensure metadata can only be called from this base URL. app.Run(); @@ -123,7 +175,7 @@ catch (Exception ex) { Console.WriteLine(ex); //todo: Logger - + return string.Empty; } @@ -133,4 +185,77 @@ async Task UdapMedatData(string s) { return s; +} + +async Task GetFhirMetadata(ResponseTransformContext responseTransformContext, + WebApplicationBuilder webApplicationBuilder) +{ + var stream = await responseTransformContext.ProxyResponse.Content.ReadAsStreamAsync(); + using var reader = new StreamReader(stream); + var body = await reader.ReadToEndAsync(); + + if (!string.IsNullOrEmpty(body)) + { + var capStatement = await new FhirJsonParser().ParseAsync(body); + var securityComponent = new CapabilityStatement.SecurityComponent(); + + securityComponent.Service.Add( + new CodeableConcept("http://fhir.udap.org/CodeSystem/capability-rest-security-service", + "UDAP", + "OAuth2 using UDAP profile (see http://www.udap.org)")); + + // + // https://build.fhir.org/ig/HL7/fhir-extensions/StructureDefinition-oauth-uris.html + // + var oauthUrlExtensions = new Extension(); + var securityExtension = new Extension("http://fhir-registry.smarthealthit.org/StructureDefinition/oauth-uris", oauthUrlExtensions); + securityExtension.Extension.Add(new Extension() { Url = "token", Value = new FhirUri(webApplicationBuilder.Configuration["Jwt:Token"]) }); + securityExtension.Extension.Add(new Extension() { Url = "authorize", Value = new FhirUri(webApplicationBuilder.Configuration["Jwt:Authorize"]) }); + securityExtension.Extension.Add(new Extension() { Url = "register", Value = new FhirUri(webApplicationBuilder.Configuration["Jwt:Register"]) }); + securityExtension.Extension.Add(new Extension() { Url = "manage", Value = new FhirUri(webApplicationBuilder.Configuration["Jwt:Manage"]) }); + securityComponent.Extension.Add(securityExtension); + capStatement.Rest.First().Security = securityComponent; + + body = new FhirJsonSerializer().SerializeToString(capStatement); + var bytes = Encoding.UTF8.GetBytes(body); + + return bytes; + } + + return null; +} + +void SetProxyHeaders(RequestTransformContext requestTransformContext) +{ + if (!requestTransformContext.HttpContext.Request.Headers.Authorization.Any()) + { + return; + } + + var bearerToken = requestTransformContext.HttpContext.Request.Headers.Authorization.First(); + + if (bearerToken == null) + { + return; + } + + foreach (var requestHeader in requestTransformContext.HttpContext.Request.Headers) + { + Console.WriteLine(requestHeader.Value); + } + + var tokenHandler = new JwtSecurityTokenHandler(); + var jsonToken = tokenHandler.ReadJwtToken(requestTransformContext.HttpContext.Request.Headers.Authorization.First()?.Replace("Bearer", "").Trim()); + var scopes = jsonToken?.Claims.Where(c => c.Type == "scope"); + var iss = jsonToken.Claims.Where(c => c.Type == "iss"); + // var sub = jsonToken.Claims.Where(c => c.Type == "sub"); // figure out what subject should be for GCP + + // Google Cloud way of passing scopes to the Fhir Server + var spaceSeparatedString = scopes?.Select(s => s.Value) + .Where(s => s != "udap") //gcp doesn't know udap Need better filter to block unknown scopes + .ToSpaceSeparatedString(); + + requestTransformContext.ProxyRequest.Headers.Add("X-Authorization-Scope", spaceSeparatedString); + requestTransformContext.ProxyRequest.Headers.Add("X-Authorization-Issuer", iss.SingleOrDefault().Value); + // context.ProxyRequest.Headers.Add("X-Authorization-Subject", sub.SingleOrDefault().Value); } \ No newline at end of file diff --git a/examples/Udap.Proxy.Server/Udap.Proxy.Server.csproj b/examples/Udap.Proxy.Server/Udap.Proxy.Server.csproj index ebdb0d99..a366cceb 100644 --- a/examples/Udap.Proxy.Server/Udap.Proxy.Server.csproj +++ b/examples/Udap.Proxy.Server/Udap.Proxy.Server.csproj @@ -12,22 +12,26 @@ - + - - + + + + + + @@ -76,6 +80,9 @@ Always + + Always + diff --git a/examples/Udap.Proxy.Server/appsettings.Development.json b/examples/Udap.Proxy.Server/appsettings.Development.json index a503b841..6a220d0f 100644 --- a/examples/Udap.Proxy.Server/appsettings.Development.json +++ b/examples/Udap.Proxy.Server/appsettings.Development.json @@ -3,8 +3,9 @@ "LogLevel": { "Default": "Information", "Microsoft": "Information", - "Yarp": "Information", - "Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler": "Information" + "Yarp": "Trace", + "Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler": "Information", + "Udap": "Trace" } }, @@ -32,17 +33,28 @@ } ] }, - "gFhirLab-route-wellknown": { + + "gFhirLab-route-wellknown-udap": { "ClusterId": "self", "Match": { - "Path": "/fhir/r4/.well-known/{**remainder}" - }, - "Transforms": [ - { - "PathPattern": ".well-known/{**remainder}" - } - ] + "Path": "/fhir/r4/.well-known/udap" + } }, + + "gFhirLab-route-wellknown-udap-commnities": { + "ClusterId": "self", + "Match": { + "Path": "/fhir/r4/.well-known/udap/communities" + } + }, + + "gFhirLab-route-wellknown-udap-communities-ashtml": { + "ClusterId": "self", + "Match": { + "Path": "/fhir/r4/.well-known/udap/communities/ashtml" + } + }, + "gFhirLab-route-base": { "ClusterId": "gFhirLab-cluster", "MetaData": { @@ -99,9 +111,17 @@ "Jwt": { "Authority": "https://host.docker.internal:5002", - "RequireHttpsMetadata": true + "RequireHttpsMetadata": true, + "Token": "https://host.docker.internal:5002/connect/token", + "Authorize": "https://host.docker.internal:5002/connect/authorize", + "Register": "https://host.docker.internal:5002/connect/register", + "Manage": "https://host.docker.internal:5002/grants" }, + "FhirUrlProxy": { + "Back": "https://healthcare.googleapis.com/v1beta1/projects/udap-idp/locations/us-west1/datasets/gFhirLab/fhirStores/fhirlabs_open/fhir", + "Front": "https://localhost:7074/fhir/r4" + }, "UdapMetadataOptions": { "Enabled": true, diff --git a/examples/Udap.Proxy.Server/appsettings.json b/examples/Udap.Proxy.Server/appsettings.json index 37c26018..309f6ae8 100644 --- a/examples/Udap.Proxy.Server/appsettings.json +++ b/examples/Udap.Proxy.Server/appsettings.json @@ -5,5 +5,14 @@ "Microsoft.AspNetCore": "Warning" } }, - "AllowedHosts": "*" + "AllowedHosts": "*", + + "Kestrel": { + "Certificates": { + "Default": { + "Path": "host.docker.internal.pfx", + "Password": "udap-test" + } + } + } } diff --git a/examples/WeatherApi/WeatherApi.csproj b/examples/WeatherApi/WeatherApi.csproj index 1ce67b90..b10d2ee5 100644 --- a/examples/WeatherApi/WeatherApi.csproj +++ b/examples/WeatherApi/WeatherApi.csproj @@ -10,7 +10,7 @@ - + diff --git a/examples/clients/1_UdapClientMetadata/1_UdapClientMetadata.csproj b/examples/clients/1_UdapClientMetadata/1_UdapClientMetadata.csproj index aeeb0228..06638fdf 100644 --- a/examples/clients/1_UdapClientMetadata/1_UdapClientMetadata.csproj +++ b/examples/clients/1_UdapClientMetadata/1_UdapClientMetadata.csproj @@ -36,8 +36,8 @@ - - + + diff --git a/examples/clients/2_UdapClientMetadata/2_UdapClientMetadata.csproj b/examples/clients/2_UdapClientMetadata/2_UdapClientMetadata.csproj index 470bcc4c..5da31d50 100644 --- a/examples/clients/2_UdapClientMetadata/2_UdapClientMetadata.csproj +++ b/examples/clients/2_UdapClientMetadata/2_UdapClientMetadata.csproj @@ -36,8 +36,8 @@ - - + + diff --git a/migrations/UdapDb.Postgres/SeedData.Auth.Server.cs b/migrations/UdapDb.Postgres/SeedData.Auth.Server.cs index ff33c77c..5da03710 100644 --- a/migrations/UdapDb.Postgres/SeedData.Auth.Server.cs +++ b/migrations/UdapDb.Postgres/SeedData.Auth.Server.cs @@ -433,10 +433,10 @@ public static async Task EnsureSeedData(string connectionString, string cer // // udap // - if (configDbContext.IdentityResources.All(i => i.Name != UdapConstants.StandardScopes.Udap)) + if (configDbContext.ApiScopes.All(i => i.Name != UdapConstants.StandardScopes.Udap)) { - var udapIdentity = new UdapIdentityResources.Udap(); - configDbContext.IdentityResources.Add(udapIdentity.ToEntity()); + var udapIdentity = new UdapApiScopes.Udap(); + configDbContext.ApiScopes.Add(udapIdentity.ToEntity()); await configDbContext.SaveChangesAsync(); } @@ -541,7 +541,7 @@ private static async Task SeedFhirScopes( var apiScope = new ApiScope(scopeName); apiScope.ShowInDiscoveryDocument = false; - if (apiScope.Name.StartsWith("patient/*.")) + if (apiScope.Name.StartsWith("user/*.")) { apiScope.ShowInDiscoveryDocument = true; apiScope.Enabled = false; diff --git a/migrations/UdapDb.Postgres/SeedData.Identity.Provider.cs b/migrations/UdapDb.Postgres/SeedData.Identity.Provider.cs index d6a0df1d..e4c06d3b 100644 --- a/migrations/UdapDb.Postgres/SeedData.Identity.Provider.cs +++ b/migrations/UdapDb.Postgres/SeedData.Identity.Provider.cs @@ -246,10 +246,10 @@ public static async Task EnsureSeedData(string connectionString, string cer // // udap // - if (configDbContext.IdentityResources.All(i => i.Name != UdapConstants.StandardScopes.Udap)) + if (configDbContext.ApiScopes.All(i => i.Name != UdapConstants.StandardScopes.Udap)) { - var udapIdentity = new UdapIdentityResources.Udap(); - configDbContext.IdentityResources.Add(udapIdentity.ToEntity()); + var udapIdentity = new UdapApiScopes.Udap(); + configDbContext.ApiScopes.Add(udapIdentity.ToEntity()); await configDbContext.SaveChangesAsync(); } diff --git a/migrations/UdapDb.Postgres/SeedData.Identity.Provider2.cs b/migrations/UdapDb.Postgres/SeedData.Identity.Provider2.cs index d94bd004..f42b0974 100644 --- a/migrations/UdapDb.Postgres/SeedData.Identity.Provider2.cs +++ b/migrations/UdapDb.Postgres/SeedData.Identity.Provider2.cs @@ -183,10 +183,10 @@ public static async Task EnsureSeedData(string connectionString, string cer // // udap // - if (configDbContext.IdentityResources.All(i => i.Name != UdapConstants.StandardScopes.Udap)) + if (configDbContext.ApiScopes.All(i => i.Name != UdapConstants.StandardScopes.Udap)) { - var udapIdentity = new UdapIdentityResources.Udap(); - configDbContext.IdentityResources.Add(udapIdentity.ToEntity()); + var udapIdentity = new UdapApiScopes.Udap(); + configDbContext.ApiScopes.Add(udapIdentity.ToEntity()); await configDbContext.SaveChangesAsync(); } diff --git a/migrations/UdapDb.Postgres/Seed_GCP_Auth_Server.cs b/migrations/UdapDb.Postgres/Seed_GCP_Auth_Server.cs index 75d68303..d8f66c75 100644 --- a/migrations/UdapDb.Postgres/Seed_GCP_Auth_Server.cs +++ b/migrations/UdapDb.Postgres/Seed_GCP_Auth_Server.cs @@ -48,19 +48,22 @@ public static async Task EnsureSeedData(string connectionString, string cer services.AddOperationalDbContext(options => { options.ConfigureDbContext = db => db.UseNpgsql(connectionString, - sql => sql.MigrationsAssembly(typeof(Program).Assembly.FullName)); + sql => sql.MigrationsAssembly(typeof(Program).Assembly.FullName) + .MigrationsHistoryTable("__migrations_history", "udap")); }); services.AddConfigurationDbContext(options => { options.ConfigureDbContext = db => db.UseNpgsql(connectionString, - sql => sql.MigrationsAssembly(typeof(Program).Assembly.FullName)); + sql => sql.MigrationsAssembly(typeof(Program).Assembly.FullName) + .MigrationsHistoryTable("__migrations_history", "udap")); }); services.AddScoped(); services.AddUdapDbContext(options => { options.UdapDbContext = db => db.UseNpgsql(connectionString, - sql => sql.MigrationsAssembly(typeof(Program).Assembly.FullName)); + sql => sql.MigrationsAssembly(typeof(Program).Assembly.FullName) + .MigrationsHistoryTable("__migrations_history", "udap")); }); await using var serviceProvider = services.BuildServiceProvider(); @@ -75,12 +78,12 @@ public static async Task EnsureSeedData(string connectionString, string cer var clientRegistrationStore = serviceScope.ServiceProvider.GetRequiredService(); - - if (!udapContext.Communities.Any(c => c.Name == "http://localhost")) + + if (!udapContext.Communities.Any(c => c.Name == "udap://stage.healthtogo.me/")) { - var community = new Community { Name = "http://localhost" }; + var community = new Community { Name = "udap://stage.healthtogo.me/" }; community.Enabled = true; - community.Default = false; + community.Default = true; udapContext.Communities.Add(community); await udapContext.SaveChangesAsync(); } @@ -89,7 +92,7 @@ public static async Task EnsureSeedData(string connectionString, string cer { var community = new Community { Name = "udap://fhirlabs.net/" }; community.Enabled = true; - community.Default = true; + community.Default = false; udapContext.Communities.Add(community); await udapContext.SaveChangesAsync(); } @@ -156,56 +159,32 @@ public static async Task EnsureSeedData(string connectionString, string cer // - // Anchor localhost_community + // Anchor for Community udap://stage.healthtogo.me/ // - var anchorLocalhostCert = new X509Certificate2( - Path.Combine(assemblyPath!, certStoreBasePath, "localhost_fhirlabs_community1/caLocalhostCert.cer")); + var emrDirectTestCA = new X509Certificate2( + Path.Combine(assemblyPath!, certStoreBasePath, "EmrDirect/EMRDirectTestCA.crt")); - if ((await clientRegistrationStore.GetAnchors("http://localhost")) - .All(a => a.Thumbprint != anchorLocalhostCert.Thumbprint)) + if ((await clientRegistrationStore.GetAnchors("udap://stage.healthtogo.me/")) + .All(a => a.Thumbprint != emrDirectTestCA.Thumbprint)) { - var community = udapContext.Communities.Single(c => c.Name == "http://localhost"); - var anchor = new Anchor + var community = udapContext.Communities.Single(c => c.Name == "udap://stage.healthtogo.me/"); + + anchor = new Anchor { - BeginDate = anchorLocalhostCert.NotBefore.ToUniversalTime(), - EndDate = anchorLocalhostCert.NotAfter.ToUniversalTime(), - Name = anchorLocalhostCert.Subject, + BeginDate = emrDirectTestCA.NotBefore.ToUniversalTime(), + EndDate = emrDirectTestCA.NotAfter.ToUniversalTime(), + Name = emrDirectTestCA.Subject, Community = community, - X509Certificate = anchorLocalhostCert.ToPemFormat(), - Thumbprint = anchorLocalhostCert.Thumbprint, + X509Certificate = emrDirectTestCA.ToPemFormat(), + Thumbprint = emrDirectTestCA.Thumbprint, Enabled = true }; - udapContext.Anchors.Add(anchor); + udapContext.Anchors.Add(anchor); await udapContext.SaveChangesAsync(); - - // - // Intermediate surefhirlabs_community - // - var x509Certificate2Collection = await clientRegistrationStore.GetIntermediateCertificates(); - - intermediateCert = new X509Certificate2( - Path.Combine(assemblyPath!, certStoreBasePath, "localhost_fhirlabs_community1/intermediates/intermediateLocalhostCert.cer")); - - if (x509Certificate2Collection != null && x509Certificate2Collection.ToList() - .All(r => r.Thumbprint != intermediateCert.Thumbprint)) - { - - udapContext.IntermediateCertificates.Add(new Intermediate - { - BeginDate = intermediateCert.NotBefore.ToUniversalTime(), - EndDate = intermediateCert.NotAfter.ToUniversalTime(), - Name = intermediateCert.Subject, - X509Certificate = intermediateCert.ToPemFormat(), - Thumbprint = intermediateCert.Thumbprint, - Enabled = true, - Anchor = anchor - }); - - await udapContext.SaveChangesAsync(); - } } + Func treatmentSpecification = r => r is "Patient" or "AllergyIntolerance" or "Condition" or "Encounter"; await SeedFhirScopes(configDbContext, Hl7ModelInfoExtensions.BuildHl7FhirV1Scopes("patient", treatmentSpecification), 1); @@ -242,10 +221,10 @@ public static async Task EnsureSeedData(string connectionString, string cer // // udap // - if (configDbContext.IdentityResources.All(i => i.Name != UdapConstants.StandardScopes.Udap)) + if (configDbContext.ApiScopes.All(i => i.Name != UdapConstants.StandardScopes.Udap)) { - var udapIdentity = new UdapIdentityResources.Udap(); - configDbContext.IdentityResources.Add(udapIdentity.ToEntity()); + var udapIdentity = new UdapApiScopes.Udap(); + configDbContext.ApiScopes.Add(udapIdentity.ToEntity()); await configDbContext.SaveChangesAsync(); } @@ -308,7 +287,7 @@ private static async Task SeedFhirScopes(NpgsqlConfigurationDbContext configDbCo { var apiScope = new ApiScope(scopeName); apiScope.ShowInDiscoveryDocument = false; - if (apiScope.Name.StartsWith("patient/*.")) + if (apiScope.Name.StartsWith("user/*.")) { apiScope.ShowInDiscoveryDocument = true; apiScope.Enabled = false; diff --git a/migrations/UdapDb.Postgres/Seed_GCP_Idp1.cs b/migrations/UdapDb.Postgres/Seed_GCP_Idp1.cs index bb2064f3..ac007397 100644 --- a/migrations/UdapDb.Postgres/Seed_GCP_Idp1.cs +++ b/migrations/UdapDb.Postgres/Seed_GCP_Idp1.cs @@ -146,10 +146,10 @@ public static async Task EnsureSeedData(string connectionString, string cer // // udap // - if (configDbContext.IdentityResources.All(i => i.Name != UdapConstants.StandardScopes.Udap)) + if (configDbContext.ApiScopes.All(i => i.Name != UdapConstants.StandardScopes.Udap)) { - var udapIdentity = new UdapIdentityResources.Udap(); - configDbContext.IdentityResources.Add(udapIdentity.ToEntity()); + var udapIdentity = new UdapApiScopes.Udap(); + configDbContext.ApiScopes.Add(udapIdentity.ToEntity()); await configDbContext.SaveChangesAsync(); } diff --git a/migrations/UdapDb.Postgres/Seed_GCP_Idp2.cs b/migrations/UdapDb.Postgres/Seed_GCP_Idp2.cs index a015fbbb..eb132c7f 100644 --- a/migrations/UdapDb.Postgres/Seed_GCP_Idp2.cs +++ b/migrations/UdapDb.Postgres/Seed_GCP_Idp2.cs @@ -238,10 +238,10 @@ public static async Task EnsureSeedData(string connectionString, string cer // // udap // - if (configDbContext.IdentityResources.All(i => i.Name != UdapConstants.StandardScopes.Udap)) + if (configDbContext.ApiScopes.All(i => i.Name != UdapConstants.StandardScopes.Udap)) { - var udapIdentity = new UdapIdentityResources.Udap(); - configDbContext.IdentityResources.Add(udapIdentity.ToEntity()); + var udapIdentity = new UdapApiScopes.Udap(); + configDbContext.ApiScopes.Add(udapIdentity.ToEntity()); await configDbContext.SaveChangesAsync(); } diff --git a/migrations/UdapDb.Postgres/UdapDb.Postgres.csproj b/migrations/UdapDb.Postgres/UdapDb.Postgres.csproj index 832010cc..8594cb5b 100644 --- a/migrations/UdapDb.Postgres/UdapDb.Postgres.csproj +++ b/migrations/UdapDb.Postgres/UdapDb.Postgres.csproj @@ -13,14 +13,14 @@ - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/migrations/UdapDb.SqlServer/SeedData.Auth.Server.cs b/migrations/UdapDb.SqlServer/SeedData.Auth.Server.cs index 39fc440d..5ea21a3a 100644 --- a/migrations/UdapDb.SqlServer/SeedData.Auth.Server.cs +++ b/migrations/UdapDb.SqlServer/SeedData.Auth.Server.cs @@ -433,10 +433,10 @@ public static async Task EnsureSeedData(string connectionString, string cer // // udap // - if (configDbContext.IdentityResources.All(i => i.Name != UdapConstants.StandardScopes.Udap)) + if (configDbContext.ApiScopes.All(i => i.Name != UdapConstants.StandardScopes.Udap)) { - var udapIdentity = new UdapIdentityResources.Udap(); - configDbContext.IdentityResources.Add(udapIdentity.ToEntity()); + var udapIdentity = new UdapApiScopes.Udap(); + configDbContext.ApiScopes.Add(udapIdentity.ToEntity()); await configDbContext.SaveChangesAsync(); } @@ -523,7 +523,7 @@ private static async Task SeedFhirScopes( var apiScope = new ApiScope(scopeName); apiScope.ShowInDiscoveryDocument = false; - if (apiScope.Name.StartsWith("patient/*.")) + if (apiScope.Name.StartsWith("user/*.")) { apiScope.ShowInDiscoveryDocument = true; apiScope.Enabled = false; diff --git a/migrations/UdapDb.SqlServer/SeedData.Identity.Provider.cs b/migrations/UdapDb.SqlServer/SeedData.Identity.Provider.cs index 765649b5..af365bfd 100644 --- a/migrations/UdapDb.SqlServer/SeedData.Identity.Provider.cs +++ b/migrations/UdapDb.SqlServer/SeedData.Identity.Provider.cs @@ -246,10 +246,10 @@ public static async Task EnsureSeedData(string connectionString, string cer // // udap // - if (configDbContext.IdentityResources.All(i => i.Name != UdapConstants.StandardScopes.Udap)) + if (configDbContext.ApiScopes.All(i => i.Name != UdapConstants.StandardScopes.Udap)) { - var udapIdentity = new UdapIdentityResources.Udap(); - configDbContext.IdentityResources.Add(udapIdentity.ToEntity()); + var udapIdentity = new UdapApiScopes.Udap(); + configDbContext.ApiScopes.Add(udapIdentity.ToEntity()); await configDbContext.SaveChangesAsync(); } diff --git a/migrations/UdapDb.SqlServer/SeedData.Identity.Provider2.cs b/migrations/UdapDb.SqlServer/SeedData.Identity.Provider2.cs index e5a61337..5baf2c93 100644 --- a/migrations/UdapDb.SqlServer/SeedData.Identity.Provider2.cs +++ b/migrations/UdapDb.SqlServer/SeedData.Identity.Provider2.cs @@ -183,10 +183,10 @@ public static async Task EnsureSeedData(string connectionString, string cer // // udap // - if (configDbContext.IdentityResources.All(i => i.Name != UdapConstants.StandardScopes.Udap)) + if (configDbContext.ApiScopes.All(i => i.Name != UdapConstants.StandardScopes.Udap)) { - var udapIdentity = new UdapIdentityResources.Udap(); - configDbContext.IdentityResources.Add(udapIdentity.ToEntity()); + var udapIdentity = new UdapApiScopes.Udap(); + configDbContext.ApiScopes.Add(udapIdentity.ToEntity()); await configDbContext.SaveChangesAsync(); } diff --git a/migrations/UdapDb.SqlServer/Seed_GCP_Auth_Server.cs b/migrations/UdapDb.SqlServer/Seed_GCP_Auth_Server.cs index 618cb6d0..f869a0f0 100644 --- a/migrations/UdapDb.SqlServer/Seed_GCP_Auth_Server.cs +++ b/migrations/UdapDb.SqlServer/Seed_GCP_Auth_Server.cs @@ -242,10 +242,10 @@ public static async Task EnsureSeedData(string connectionString, string cer // // udap // - if (configDbContext.IdentityResources.All(i => i.Name != UdapConstants.StandardScopes.Udap)) + if (configDbContext.ApiScopes.All(i => i.Name != UdapConstants.StandardScopes.Udap)) { - var udapIdentity = new UdapIdentityResources.Udap(); - configDbContext.IdentityResources.Add(udapIdentity.ToEntity()); + var udapIdentity = new UdapApiScopes.Udap(); + configDbContext.ApiScopes.Add(udapIdentity.ToEntity()); await configDbContext.SaveChangesAsync(); } @@ -322,7 +322,7 @@ private static async Task SeedFhirScopes(ConfigurationDbContext configDbContext, { var apiScope = new ApiScope(scopeName); apiScope.ShowInDiscoveryDocument = false; - if (apiScope.Name.StartsWith("patient/*.")) + if (apiScope.Name.StartsWith("user/*.")) { apiScope.ShowInDiscoveryDocument = true; apiScope.Enabled = false; diff --git a/migrations/UdapDb.SqlServer/Seed_GCP_Idp1.cs b/migrations/UdapDb.SqlServer/Seed_GCP_Idp1.cs index 4e156872..747f17b2 100644 --- a/migrations/UdapDb.SqlServer/Seed_GCP_Idp1.cs +++ b/migrations/UdapDb.SqlServer/Seed_GCP_Idp1.cs @@ -146,10 +146,10 @@ public static async Task EnsureSeedData(string connectionString, string cer // // udap // - if (configDbContext.IdentityResources.All(i => i.Name != UdapConstants.StandardScopes.Udap)) + if (configDbContext.ApiScopes.All(i => i.Name != UdapConstants.StandardScopes.Udap)) { - var udapIdentity = new UdapIdentityResources.Udap(); - configDbContext.IdentityResources.Add(udapIdentity.ToEntity()); + var udapIdentity = new UdapApiScopes.Udap(); + configDbContext.ApiScopes.Add(udapIdentity.ToEntity()); await configDbContext.SaveChangesAsync(); } diff --git a/migrations/UdapDb.SqlServer/Seed_GCP_Idp2.cs b/migrations/UdapDb.SqlServer/Seed_GCP_Idp2.cs index 3d1873a9..9e918845 100644 --- a/migrations/UdapDb.SqlServer/Seed_GCP_Idp2.cs +++ b/migrations/UdapDb.SqlServer/Seed_GCP_Idp2.cs @@ -238,10 +238,10 @@ public static async Task EnsureSeedData(string connectionString, string cer // // udap // - if (configDbContext.IdentityResources.All(i => i.Name != UdapConstants.StandardScopes.Udap)) + if (configDbContext.ApiScopes.All(i => i.Name != UdapConstants.StandardScopes.Udap)) { - var udapIdentity = new UdapIdentityResources.Udap(); - configDbContext.IdentityResources.Add(udapIdentity.ToEntity()); + var udapIdentity = new UdapApiScopes.Udap(); + configDbContext.ApiScopes.Add(udapIdentity.ToEntity()); await configDbContext.SaveChangesAsync(); } diff --git a/migrations/UdapDb.SqlServer/UdapDb.SqlServer.csproj b/migrations/UdapDb.SqlServer/UdapDb.SqlServer.csproj index b075d454..3826757b 100644 --- a/migrations/UdapDb.SqlServer/UdapDb.SqlServer.csproj +++ b/migrations/UdapDb.SqlServer/UdapDb.SqlServer.csproj @@ -12,14 +12,14 @@ - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + +