diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 96be437d..dbe78984 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "dotnet-ef": { - "version": "7.0.11", + "version": "7.0.13", "commands": [ "dotnet-ef" ] diff --git a/Directory.Packages.props b/Directory.Packages.props index 95071074..be31cc2f 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -4,12 +4,13 @@ + - + - + @@ -21,17 +22,17 @@ - - - + + + - + - + @@ -43,6 +44,6 @@ - + \ No newline at end of file diff --git a/Udap.Client/Client/Extensions/HttpClientTokenRequestExtensions.cs b/Udap.Client/Client/Extensions/HttpClientTokenRequestExtensions.cs index a27eb7ec..f0981d0b 100644 --- a/Udap.Client/Client/Extensions/HttpClientTokenRequestExtensions.cs +++ b/Udap.Client/Client/Extensions/HttpClientTokenRequestExtensions.cs @@ -110,12 +110,12 @@ internal static async Task RequestTokenAsync(this HttpMessageInvo /// /// The client. /// The request. - /// The cancellation token. + /// The cancellation token. /// public static async Task ExchangeCodeForAuthTokenResponse( this HttpMessageInvoker client, AuthorizationCodeTokenRequest request, - CancellationToken tokenRequest = default) + CancellationToken cancellationToken = default) { var clone = request.Clone(); @@ -136,9 +136,9 @@ public static async Task ExchangeCodeForAuthTokenResponse( clone.Prepare(); clone.Method = HttpMethod.Post; - var response = await client.SendAsync(clone, tokenRequest); + var response = await client.SendAsync(clone, cancellationToken); - var body = await response.Content.ReadAsStringAsync(tokenRequest); + var body = await response.Content.ReadAsStringAsync(cancellationToken); return response.IsSuccessStatusCode switch { diff --git a/Udap.Client/Client/IUdapClient.cs b/Udap.Client/Client/IUdapClient.cs index 0799f825..3fb1dde0 100644 --- a/Udap.Client/Client/IUdapClient.cs +++ b/Udap.Client/Client/IUdapClient.cs @@ -25,21 +25,24 @@ public interface IUdapClient : IUdapClientEvents Task ValidateResource( string baseUrl, string? community = null, - DiscoveryPolicy? discoveryPolicy = null); + DiscoveryPolicy? discoveryPolicy = null, + CancellationToken token = default); Task ValidateResource( string baseUrl, ITrustAnchorStore? trustAnchorStore, string? community = null, - DiscoveryPolicy? discoveryPolicy = null); + DiscoveryPolicy? discoveryPolicy = null, + CancellationToken token = default); UdapMetadata? UdapDynamicClientRegistrationDocument { get; set; } UdapMetadata? UdapServerMetaData { get; set; } - + /// /// Register a TieredClient in the Authorization Server. - /// Currently it is not SAN and Community aware. It picks the first SAN. + /// Currently it is not SAN aware. It picks the first SAN. + /// To pick a different community the client can add a community query parameter to the . /// /// /// @@ -51,7 +54,79 @@ Task RegisterTieredClient(string redirect string scopes, CancellationToken token = default); + /// + /// Register a UdapClient in the Authorization Server with authorization_code flow. + /// + /// + /// + /// + /// + /// If issuer is supplied it will match try to match to a valid URI based subject alternative name from the X509Certificate + /// + /// + Task RegisterAuthCodeClient( + IEnumerable certificates, + string scopes, + string logo, + ICollection redirectUrl, + string? issuer = null, + CancellationToken token = default); + + /// + /// Register a UdapClient in the Authorization Server with authorization_code flow. + /// + /// + /// + /// optional + /// + /// If issuer is supplied it will match try to match to a valid URI based subject alternative name from the X509Certificate + /// + /// + Task RegisterAuthCodeClient( + X509Certificate2 certificate, + string scopes, + string logo, + ICollection redirectUrl, + string? issuer = null, + CancellationToken token = default); + + /// + /// Register a UdapClient in the Authorization Server with client_credentials flow. + /// + /// + /// + /// + /// If issuer is supplied it will match try to match to a valid URI based subject alternative name from the X509Certificate + /// + /// + Task RegisterClientCredentialsClient( + IEnumerable certificates, + string scopes, + string? issuer = null, + string? logo = null, + CancellationToken token = default); + + /// + /// Register a UdapClient in the Authorization Server with client_credentials flow. + /// + /// + /// + /// optional + /// If issuer is supplied it will match try to match to a valid URI based subject alternative name from the X509Certificate + /// + /// + Task RegisterClientCredentialsClient( + X509Certificate2 certificate, + string scopes, + string? issuer = null, + string? logo = null, + CancellationToken token = default); + + Task ExchangeCodeForTokenResponse(UdapAuthorizationCodeTokenRequest tokenRequest, CancellationToken token = default); + Task ExchangeCodeForAuthTokenResponse(UdapAuthorizationCodeTokenRequest tokenRequest, CancellationToken token = default); Task?> ResolveJwtKeys(DiscoveryDocumentRequest? request = null, CancellationToken cancellationToken = default); + + Task ResolveOpenIdConfig(DiscoveryDocumentRequest? request = null, CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/Udap.Client/Client/UdapClient.cs b/Udap.Client/Client/UdapClient.cs index 64bdc655..fbe01eb4 100644 --- a/Udap.Client/Client/UdapClient.cs +++ b/Udap.Client/Client/UdapClient.cs @@ -8,18 +8,12 @@ #endregion using System.Net; -using System.Net.Http; -using System.Text.Json.Serialization; -using System.Text.Json; -using System.Threading.Tasks; - -using System.Net.Http.Headers; using System.Net.Http.Json; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text.Json; using IdentityModel.Client; -// using Microsoft.AspNetCore.Authentication.OAuth; +using Microsoft.AspNetCore.Authentication.OAuth; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; @@ -32,7 +26,10 @@ using Udap.Model.Access; using Udap.Model.Registration; using Udap.Model.Statement; -using Microsoft.AspNetCore.Authentication.OAuth; +#if NET7_0_OR_GREATER +using System.Net.Http.Headers; +#endif +// using Microsoft.AspNetCore.Authentication.OAuth; namespace Udap.Client.Client { @@ -95,87 +92,127 @@ public async Task RegisterTieredClient(st { if (this.UdapServerMetaData == null) { - throw new Exception("Tiered OAuth: UdapServerMetaData is null. Call ValidateResource first."); + throw new Exception("Tiered OAuth Client: UdapServerMetaData is null. Call ValidateResource first."); } try { - var x509Certificates = certificates.ToList(); - if (certificates == null || !x509Certificates.Any()) + var resultDocument = await RegisterAuthCodeFlow(certificates, scopes, _udapClientOptions.TieredOAuthClientLogo, new List{ redirectUrl }, null, token); + + if(string.IsNullOrEmpty(resultDocument.GetError())) { - throw new Exception("Tiered OAuth: No client certificates provided."); + _logger.LogWarning("Tiered OAuth Client: Unable to register client to {RegistrationEndpoint}", this.UdapServerMetaData?.RegistrationEndpoint); } - foreach (var clientCert in x509Certificates) + return resultDocument; + } + catch(Exception ex) + { + _logger.LogError(ex, "Tiered OAuth Client: Unable to register client to {RegistrationEndpoint}", + this.UdapServerMetaData?.RegistrationEndpoint); + throw; + } + } + + /// + public async Task RegisterAuthCodeClient( + IEnumerable certificates, + string scopes, + string logo, + ICollection redirectUrl, + string? issuer, + CancellationToken token = default) + { + if (this.UdapServerMetaData == null) + { + throw new Exception("UdapClient: UdapServerMetaData is null. Call ValidateResource first."); + } + + try + { + var resultDocument = await RegisterAuthCodeFlow(certificates, scopes, logo, redirectUrl, issuer, token); + + if (string.IsNullOrEmpty(resultDocument.GetError())) { - _logger.LogDebug($"Using certificate {clientCert.SubjectName.Name} [ {clientCert.Thumbprint} ]"); - - var document = UdapDcrBuilderForAuthorizationCode - .Create(clientCert) - .WithAudience(this.UdapServerMetaData?.RegistrationEndpoint) - .WithExpiration(TimeSpan.FromMinutes(5)) - .WithJwtId() - .WithClientName(_udapClientOptions.ClientName) - //Todo get logo from client registration, maybe Client object. But still nee to retain logo in clientproperties during registration - .WithLogoUri("https://udaped.fhirlabs.net/images/udap-dotnet-auth-server.png") - .WithContacts(_udapClientOptions.Contacts) - .WithTokenEndpointAuthMethod(UdapConstants.RegistrationDocumentValues - .TokenEndpointAuthMethodValue) - .WithScope(scopes) - .WithResponseTypes(new List { "code" }) - .WithRedirectUrls(new List { redirectUrl }) - .Build(); - - - var signedSoftwareStatement = - SignedSoftwareStatementBuilder - .Create(clientCert, document) - .Build(); - - var requestBody = new UdapRegisterRequest - ( - signedSoftwareStatement, - UdapConstants.UdapVersionsSupportedValue - // new string[] { } - ); - - // New StringContent constructor taking a MediaTypeHeaderValue to ensure CharSet can be controlled - // by the caller. - // Good historical conversations. - // https://github.com/dotnet/runtime/pull/63231 - // https://github.com/dotnet/runtime/issues/17036 - // -#if NET7_0_OR_GREATER - var content = new StringContent( - JsonSerializer.Serialize(requestBody), - new MediaTypeHeaderValue("application/json") ); -#else - var content = new StringContent(JsonSerializer.Serialize(requestBody), null, "application/json"); - content.Headers.ContentType!.CharSet = string.Empty; - #endif + _logger.LogWarning("UdapClient: Unable to register authorization_code client to {RegistrationEndpoint}", this.UdapServerMetaData?.RegistrationEndpoint); + } + + return resultDocument; + } + catch (Exception ex) + { + _logger.LogError(ex, "UdapClient: Unable to register authorization_code client to {RegistrationEndpoint}", + this.UdapServerMetaData?.RegistrationEndpoint); + throw; + } + } - var response = await _httpClient.PostAsync(this.UdapServerMetaData?.RegistrationEndpoint, content, token); + /// + public async Task RegisterAuthCodeClient( + X509Certificate2 certificate, + string scopes, + string logo, + ICollection redirectUrl, + string? issuer, + CancellationToken token = default) + { + return await this.RegisterAuthCodeClient( + new List { certificate }, + scopes, + logo, + redirectUrl, + issuer, + token + ); + } - if (((int)response.StatusCode) < 500) - { - var resultDocument = - await response.Content.ReadFromJsonAsync(cancellationToken: token); + /// + public async Task RegisterClientCredentialsClient( + IEnumerable certificates, + string scopes, + string? issuer, + string? logo, + CancellationToken token = default) + { + if (this.UdapServerMetaData == null) + { + throw new Exception("UdapClient: UdapServerMetaData is null. Call ValidateResource first."); + } - return resultDocument; - } + try + { + var resultDocument = await RegisterClientCredFlow(certificates, scopes, logo, issuer, token); + + if (string.IsNullOrEmpty(resultDocument.GetError())) + { + _logger.LogWarning("UdapClient: Unable to register client_credentials client to {RegistrationEndpoint}", this.UdapServerMetaData?.RegistrationEndpoint); } + + return resultDocument; } - catch(Exception ex) + catch (Exception ex) { - _logger.LogError(ex, "Tiered OAuth: Unable to register client to {RegistrationEndpoint}", - this.UdapServerMetaData?.RegistrationEndpoint); + _logger.LogError(ex, "UdapClient: Unable to register client_credentials client to {RegistrationEndpoint}", + this.UdapServerMetaData?.RegistrationEndpoint); throw; } + } - - _logger.LogWarning("Tiered OAuth: Unable to register client to {RegistrationEndpoint}", this.UdapServerMetaData?.RegistrationEndpoint); - // Todo: typed exception? or null return etc... - throw new Exception($"Tiered OAuth: Unable to register client to {this.UdapServerMetaData?.RegistrationEndpoint}"); + //// + public async Task RegisterClientCredentialsClient( + X509Certificate2 certificate, + string scopes, + string? issuer, + string? logo, + CancellationToken token = default) + { + return await this.RegisterClientCredentialsClient( + new List { certificate }, + scopes, + logo, + issuer, + token + ); } /// @@ -183,7 +220,7 @@ public async Task RegisterTieredClient(st /// /// The request. /// The cancellation token. - /// + /// public async Task ExchangeCodeForTokenResponse( UdapAuthorizationCodeTokenRequest tokenRequest, CancellationToken token = default) @@ -218,14 +255,16 @@ public async Task ExchangeCodeForAuthTokenResponse( /// /// /// + /// /// public Task ValidateResource( string baseUrl, ITrustAnchorStore? trustAnchorStore, string? community = null, - DiscoveryPolicy? discoveryPolicy = null) + DiscoveryPolicy? discoveryPolicy = null, + CancellationToken token = default) { - return InternalValidateResource(baseUrl, trustAnchorStore, community, discoveryPolicy); + return InternalValidateResource(baseUrl, trustAnchorStore, community, discoveryPolicy, token); } /// @@ -234,22 +273,25 @@ public Task ValidateResource( /// /// /// + /// /// /// public async Task ValidateResource( string baseUrl, string? community, - DiscoveryPolicy? discoveryPolicy) + DiscoveryPolicy? discoveryPolicy, + CancellationToken token = default) { - return await InternalValidateResource(baseUrl, null, community, discoveryPolicy); + return await InternalValidateResource(baseUrl, null, community, discoveryPolicy, token); } private async Task InternalValidateResource( string baseUrl, ITrustAnchorStore? trustAnchorStore, string? community, - DiscoveryPolicy? discoveryPolicy) - { + DiscoveryPolicy? discoveryPolicy, + CancellationToken token = default) + { baseUrl.AssertUri(); @@ -267,7 +309,7 @@ private async Task InternalValidateResource( Address = baseUrl, Community = community, Policy = _discoveryPolicy - }); + }, cancellationToken: token); if (disco.HttpStatusCode == HttpStatusCode.OK && !disco.IsError) { @@ -296,7 +338,7 @@ private async Task InternalValidateResource( _logger.LogError(ex, "Failed validating resource metadata"); return ProtocolResponse.FromException(ex); } - } + } public async Task?> ResolveJwtKeys(DiscoveryDocumentRequest? request = null, CancellationToken cancellationToken = default) @@ -329,6 +371,21 @@ private async Task InternalValidateResource( return keys; } + public async Task ResolveOpenIdConfig(DiscoveryDocumentRequest? request = null, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(request); + + //TODO: Cache Discovery Document? + var disco = await _httpClient.GetDiscoveryDocumentAsync(request, cancellationToken: cancellationToken); + + if (disco.HttpStatusCode != HttpStatusCode.OK || disco.IsError) + { + throw new Exception("Failed to retrieve discovery document: " + disco.Error); + } + + return disco; + } + private void NotifyTokenError(string message) { @@ -346,6 +403,209 @@ private void NotifyTokenError(string message) } } } + + private async Task RegisterAuthCodeFlow( + IEnumerable certificates, + string scopes, + string logoUrl, + ICollection? redirectUrls, + string? issuer, + CancellationToken token) + { + var x509Certificates = certificates.ToList(); + if (certificates == null || !x509Certificates.Any()) + { + throw new Exception("Tiered OAuth: No client certificates provided."); + } + + if (string.IsNullOrEmpty(_udapClientOptions.ClientName)) + { + throw new ArgumentException("UdapClientOptions.ClientName is empty"); + } + + UdapDynamicClientRegistrationDocument? resultDocument; + + foreach (var clientCert in x509Certificates) + { + _logger.LogDebug($"Using certificate {clientCert.SubjectName.Name} [ {clientCert.Thumbprint} ]"); + + var builder = UdapDcrBuilderForAuthorizationCode + .Create(clientCert) + .WithAudience(this.UdapServerMetaData?.RegistrationEndpoint) + .WithExpiration(TimeSpan.FromMinutes(5)) + .WithJwtId() + .WithClientName(_udapClientOptions.ClientName) + .WithLogoUri(logoUrl) + .WithContacts(_udapClientOptions.Contacts) + .WithTokenEndpointAuthMethod(UdapConstants.RegistrationDocumentValues.TokenEndpointAuthMethodValue) + .WithScope(scopes) + .WithResponseTypes(new List { "code" }) + .WithRedirectUrls(redirectUrls); + + if (!string.IsNullOrEmpty(issuer)) + { + builder.WithIssuer(new Uri(issuer)); + } + + var document = builder.Build(); + + var signedSoftwareStatement = + SignedSoftwareStatementBuilder + .Create(clientCert, document) + .Build(); + + var requestBody = new UdapRegisterRequest + ( + signedSoftwareStatement, + UdapConstants.UdapVersionsSupportedValue + // new string[] { } + ); + + // New StringContent constructor taking a MediaTypeHeaderValue to ensure CharSet can be controlled + // by the caller. + // Good historical conversations. + // https://github.com/dotnet/runtime/pull/63231 + // https://github.com/dotnet/runtime/issues/17036 + // +#if NET7_0_OR_GREATER + var content = new StringContent( + JsonSerializer.Serialize(requestBody), + new MediaTypeHeaderValue("application/json")); +#else + var content = new StringContent(JsonSerializer.Serialize(requestBody), null, "application/json"); + content.Headers.ContentType!.CharSet = string.Empty; +#endif + + var response = await _httpClient.PostAsync(this.UdapServerMetaData?.RegistrationEndpoint, content, token); + + if (((int)response.StatusCode) < 500) + { + resultDocument = + await response.Content.ReadFromJsonAsync( + cancellationToken: token); + + if (resultDocument == null) + { + resultDocument = new UdapDynamicClientRegistrationDocument + { + { "error", "Unknown error" }, + { "error_description", response.StatusCode } + }; + } + + return resultDocument; + } + } + + resultDocument = new UdapDynamicClientRegistrationDocument + { + { "error", "Unknown error" }, + { "error_description", "Failed to register with all client certificates" } + }; + + return resultDocument; + } + + + private async Task RegisterClientCredFlow( + IEnumerable certificates, + string scopes, + string? issuer, + string? logoUrl, + CancellationToken token) + { + var x509Certificates = certificates.ToList(); + if (certificates == null || !x509Certificates.Any()) + { + throw new Exception("Tiered OAuth: No client certificates provided."); + } + + if (string.IsNullOrEmpty(_udapClientOptions.ClientName)) + { + throw new ArgumentException("UdapClientOptions.ClientName is empty"); + } + + UdapDynamicClientRegistrationDocument? resultDocument; + + foreach (var clientCert in x509Certificates) + { + _logger.LogDebug($"Using certificate {clientCert.SubjectName.Name} [ {clientCert.Thumbprint} ]"); + + var builder = UdapDcrBuilderForClientCredentials + .Create(clientCert) + .WithAudience(this.UdapServerMetaData?.RegistrationEndpoint) + .WithExpiration(TimeSpan.FromMinutes(5)) + .WithJwtId() + .WithClientName(_udapClientOptions.ClientName) + .WithLogoUri(logoUrl) + .WithContacts(_udapClientOptions.Contacts) + .WithTokenEndpointAuthMethod(UdapConstants.RegistrationDocumentValues.TokenEndpointAuthMethodValue) + .WithScope(scopes); + + if (!string.IsNullOrEmpty(issuer)) + { + builder.WithIssuer(new Uri(issuer)); + } + + var document = builder.Build(); + + var signedSoftwareStatement = + SignedSoftwareStatementBuilder + .Create(clientCert, document) + .Build(); + + var requestBody = new UdapRegisterRequest + ( + signedSoftwareStatement, + UdapConstants.UdapVersionsSupportedValue + // new string[] { } + ); + + // New StringContent constructor taking a MediaTypeHeaderValue to ensure CharSet can be controlled + // by the caller. + // Good historical conversations. + // https://github.com/dotnet/runtime/pull/63231 + // https://github.com/dotnet/runtime/issues/17036 + // +#if NET7_0_OR_GREATER + var content = new StringContent( + JsonSerializer.Serialize(requestBody), + new MediaTypeHeaderValue("application/json")); +#else + var content = new StringContent(JsonSerializer.Serialize(requestBody), null, "application/json"); + content.Headers.ContentType!.CharSet = string.Empty; +#endif + + var response = await _httpClient.PostAsync(this.UdapServerMetaData?.RegistrationEndpoint, content, token); + + if (((int)response.StatusCode) < 500) + { + resultDocument = + await response.Content.ReadFromJsonAsync( + cancellationToken: token); + + if (resultDocument == null) + { + resultDocument = new UdapDynamicClientRegistrationDocument + { + { "error", "Unknown error" }, + { "error_description", response.StatusCode } + }; + } + + return resultDocument; + } + } + + resultDocument = new UdapDynamicClientRegistrationDocument + { + { "error", "Unknown error" }, + { "error_description", "Failed to register with all client certificates" } + }; + + return resultDocument; + } + } - + } diff --git a/Udap.Client/Configuration/UdapClientOptions.cs b/Udap.Client/Configuration/UdapClientOptions.cs index 7b699a16..39aa5184 100644 --- a/Udap.Client/Configuration/UdapClientOptions.cs +++ b/Udap.Client/Configuration/UdapClientOptions.cs @@ -20,4 +20,7 @@ public class UdapClientOptions [JsonPropertyName("Headers")] public Dictionary? Headers { get; set; } + + [JsonPropertyName("TieredOAuthClientLogo")] + public string TieredOAuthClientLogo { get; set; } } diff --git a/Udap.Common/Certificates/IssuedCertificateStore.cs b/Udap.Common/Certificates/IssuedCertificateStore.cs index c729c164..1d892bf3 100644 --- a/Udap.Common/Certificates/IssuedCertificateStore.cs +++ b/Udap.Common/Certificates/IssuedCertificateStore.cs @@ -65,7 +65,7 @@ private void LoadCertificates(UdapFileCertStoreManifest manifestCurrentValue) foreach (var community in communities) { - _logger.LogInformation($"Loading Community:: Name: '{community.Name}' IdPBaseUrl: '{community.IdPBaseUrl}'"); + _logger.LogInformation($"Loading Community:: Name: '{community.Name}'"); foreach (var communityIssuer in community.IssuedCerts) { @@ -97,7 +97,6 @@ is X509BasicConstraintsExtension extension && IssuedCertificates.Add(new IssuedCertificate { Community = community.Name, - IdPBaseUrl = community.IdPBaseUrl, Certificate = x509Cert, Thumbprint = x509Cert.Thumbprint }); diff --git a/Udap.Common/Metadata/Community.cs b/Udap.Common/Metadata/Community.cs index 41be4156..a6f7cfde 100644 --- a/Udap.Common/Metadata/Community.cs +++ b/Udap.Common/Metadata/Community.cs @@ -16,11 +16,6 @@ public class Community /// public string Name { get; set; } = "Default"; - /// - /// Used to map an IdP url to the client certificate when registering with the Idp - /// - public string? IdPBaseUrl { get; set; } - /// /// Remote Idp community projection /// diff --git a/Udap.Common/Models/IssuedCertificate.cs b/Udap.Common/Models/IssuedCertificate.cs index ce276458..a8dc7224 100644 --- a/Udap.Common/Models/IssuedCertificate.cs +++ b/Udap.Common/Models/IssuedCertificate.cs @@ -16,7 +16,6 @@ public class IssuedCertificate public int Id { get; set; } public bool Enabled { get; set; } // public string Name { get; set; } = string.Empty; - public string? IdPBaseUrl { get; set; } public string Community { get; set; } = string.Empty; public X509Certificate2 Certificate { get; set; } = default!; diff --git a/Udap.Common/Models/TieredClient.cs b/Udap.Common/Models/TieredClient.cs index 3edff823..8203a0e8 100644 --- a/Udap.Common/Models/TieredClient.cs +++ b/Udap.Common/Models/TieredClient.cs @@ -12,4 +12,6 @@ public class TieredClient public int CommunityId { get; set; } public bool Enabled { get; set; } + + public string TokenEndpoint { get; set; } } \ No newline at end of file diff --git a/Udap.Metadata.Server/Configuration/DependencyInjection/ServiceCollectionExtensions.cs b/Udap.Metadata.Server/Configuration/DependencyInjection/ServiceCollectionExtensions.cs index 5b28e5db..172b1847 100644 --- a/Udap.Metadata.Server/Configuration/DependencyInjection/ServiceCollectionExtensions.cs +++ b/Udap.Metadata.Server/Configuration/DependencyInjection/ServiceCollectionExtensions.cs @@ -96,10 +96,10 @@ public static IServiceCollection AddUdapMetadataServer( //TODO: this could use some DI work... var udapMetadata = new UdapMetadata( udapMetadataOptions!, - Hl7ModelInfoExtensions - .BuildHl7FhirV1AndV2Scopes(new List { "patient", "user", "system" }) - .Where(s => s.Contains("/*")) //Just show the wild card - ); + new List + { + "openid", "patient/*.read", "user/*.read", "system/*.read", "patient/*.rs", "user/*.rs", "system/*.rs" + }); services.AddSingleton(udapMetadata); services.TryAddScoped(); diff --git a/Udap.Model/Access/AccessTokenRequestForClientCredentialsBuilder.cs b/Udap.Model/Access/AccessTokenRequestForClientCredentialsBuilder.cs index 74b75c2a..3df21b85 100644 --- a/Udap.Model/Access/AccessTokenRequestForClientCredentialsBuilder.cs +++ b/Udap.Model/Access/AccessTokenRequestForClientCredentialsBuilder.cs @@ -9,7 +9,6 @@ using System; using System.Collections.Generic; -using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Security.Cryptography.X509Certificates; using System.Text.Json; diff --git a/Udap.Model/Registration/UdapDcrBuilderForAuthorizationCode.cs b/Udap.Model/Registration/UdapDcrBuilderForAuthorizationCode.cs index d777418e..268fbdac 100644 --- a/Udap.Model/Registration/UdapDcrBuilderForAuthorizationCode.cs +++ b/Udap.Model/Registration/UdapDcrBuilderForAuthorizationCode.cs @@ -14,6 +14,7 @@ using IdentityModel; using Microsoft.IdentityModel.Tokens; using Udap.Model.Statement; +using Udap.Util.Extensions; namespace Udap.Model.Registration; @@ -72,22 +73,10 @@ public static UdapDcrBuilderForAuthorizationCode Create(X509Certificate2 cert) { return new UdapDcrBuilderForAuthorizationCode(cert, false); } - - //TODO: Safe for multi SubjectAltName scenarios - /// - /// Register or update an existing registration by subjectAltName - /// - /// - /// - public static UdapDcrBuilderForAuthorizationCode Create(X509Certificate2 cert, string subjectAltName) - { - return new UdapDcrBuilderForAuthorizationCode(cert, false); - } - + /// /// Register or update an existing registration /// - /// /// public static UdapDcrBuilderForAuthorizationCode Create() { @@ -104,18 +93,6 @@ public static UdapDcrBuilderForAuthorizationCode Cancel(X509Certificate2 cert) return new UdapDcrBuilderForAuthorizationCode(cert, true); } - //TODO: Safe for multi SubjectAltName scenarios - /// - /// Cancel an existing registration by subject alt name. - /// - /// - /// - /// - public static UdapDcrBuilderForAuthorizationCode Cancel(X509Certificate2 cert, string subjectAltName) - { - return new UdapDcrBuilderForAuthorizationCode(cert, true); - } - /// /// Cancel an existing registration. /// @@ -142,6 +119,27 @@ public UdapDcrBuilderForAuthorizationCode WithGrantType(string grantType) return this; } +#if NET6_0_OR_GREATER + /// + /// If the certificate has more than one uniformResourceIdentifier in the Subject Alternative Name + /// extension of the client certificate then this will allow one to be picked. + /// + /// + /// + public UdapDcrBuilderForAuthorizationCode WithIssuer(Uri issuer) + { + var uriNames = _certificate!.GetSubjectAltNames(n => n.TagNo == (int)X509Extensions.GeneralNameType.URI); + if (!uriNames.Select(u => u.Item2).Contains(issuer.AbsoluteUri)) + { + throw new Exception($"Certificate does not contain a URI Subject Alternative Name of, {issuer.AbsoluteUri}"); + } + _document.Issuer = issuer.AbsoluteUri; + _document.Subject = issuer.AbsoluteUri; + return this; + } + +#endif + public UdapDcrBuilderForAuthorizationCode WithAudience(string? audience) { _document.Audience = audience; @@ -182,7 +180,7 @@ public UdapDcrBuilderForAuthorizationCode WithJwtId(string? jwtId = null) return this; } - public UdapDcrBuilderForAuthorizationCode WithClientName(string? clientName) + public UdapDcrBuilderForAuthorizationCode WithClientName(string clientName) { _document.ClientName = clientName; return this; @@ -218,15 +216,13 @@ public UdapDcrBuilderForAuthorizationCode WithRedirectUrls(ICollection? return this; } - public UdapDcrBuilderForAuthorizationCode WithLogoUri(string? logoUri) + public UdapDcrBuilderForAuthorizationCode WithLogoUri(string logoUri) { - //TODO: Testing. And better technique. _ = new Uri(logoUri); _document.LogoUri = logoUri; return this; } - //TODO: should be able to build with all certs in path. public UdapDcrBuilderForAuthorizationCode WithCertificate(X509Certificate2 certificate) { _certificate = certificate; diff --git a/Udap.Model/Registration/UdapDcrBuilderForClientCredentials.cs b/Udap.Model/Registration/UdapDcrBuilderForClientCredentials.cs index 406941b1..4f0f2d0c 100644 --- a/Udap.Model/Registration/UdapDcrBuilderForClientCredentials.cs +++ b/Udap.Model/Registration/UdapDcrBuilderForClientCredentials.cs @@ -25,7 +25,7 @@ namespace Udap.Model.Registration; /// public class UdapDcrBuilderForClientCredentials { - private DateTime _now; + private readonly DateTime _now; private UdapDynamicClientRegistrationDocument _document; private X509Certificate2? _certificate; @@ -75,21 +75,10 @@ public static UdapDcrBuilderForClientCredentials Create(X509Certificate2 cert) return new UdapDcrBuilderForClientCredentials(cert, false); } - //TODO: Safe for multi SubjectAltName scenarios - /// - /// Register or update an existing registration by subjectAltName - /// - /// - /// - public static UdapDcrBuilderForClientCredentials Create(X509Certificate2 cert, string subjectAltName) - { - return new UdapDcrBuilderForClientCredentials(cert, false); - } - + /// /// Register or update an existing registration /// - /// /// public static UdapDcrBuilderForClientCredentials Create() { @@ -106,18 +95,7 @@ public static UdapDcrBuilderForClientCredentials Cancel(X509Certificate2 cert) return new UdapDcrBuilderForClientCredentials(cert, true); } - //TODO: Safe for multi SubjectAltName scenarios - /// - /// Cancel an existing registration by subject alt name. - /// - /// - /// - /// - public static UdapDcrBuilderForClientCredentials Cancel(X509Certificate2 cert, string subjectAltName) - { - return new UdapDcrBuilderForClientCredentials(cert, true); - } - + /// /// Cancel an existing registration. /// @@ -131,12 +109,7 @@ public static UdapDcrBuilderForClientCredentials Cancel() /// /// Set at construction time. /// - public DateTime Now { - get - { - return _now; - } - } + public DateTime Now => _now; #if NET6_0_OR_GREATER /// @@ -205,7 +178,7 @@ public UdapDcrBuilderForClientCredentials WithClientName(string clientName) return this; } - public UdapDcrBuilderForClientCredentials WithContacts(ICollection contacts) + public UdapDcrBuilderForClientCredentials WithContacts(ICollection? contacts) { _document.Contacts = contacts; return this; @@ -223,14 +196,18 @@ public UdapDcrBuilderForClientCredentials WithScope(string? scope) return this; } - public UdapDcrBuilderForClientCredentials WithLogoUri(string logoUri) + public UdapDcrBuilderForClientCredentials WithLogoUri(string? logoUri) { - //TODO: Testing. And better technique. + if (string.IsNullOrEmpty(logoUri)) + { + return this; + } + _ = new Uri(logoUri); + _document.LogoUri = logoUri; return this; } - //TODO: should be able to build with all certs in path. public UdapDcrBuilderForClientCredentials WithCertificate(X509Certificate2 certificate) { _certificate = certificate; diff --git a/Udap.Model/Registration/UdapDynamicClientRegistrationDocument.cs b/Udap.Model/Registration/UdapDynamicClientRegistrationDocument.cs index 585eae81..7ab53c1e 100644 --- a/Udap.Model/Registration/UdapDynamicClientRegistrationDocument.cs +++ b/Udap.Model/Registration/UdapDynamicClientRegistrationDocument.cs @@ -44,7 +44,6 @@ public class UdapDynamicClientRegistrationDocument : Dictionary, private long _issuedAt; private string? _jwtId; private string? _clientName; - private Uri? _clientUri; private ICollection? _redirectUris = new List(); private string? _logoUri; private ICollection? _contacts = new HashSet(); @@ -257,29 +256,34 @@ public string? ClientName } } - /// - /// Web page providing information about the client. - /// See - /// - [JsonPropertyName(UdapConstants.RegistrationDocumentValues.ClientUri)] - public Uri? ClientUri { - get - { - if (_clientUri == null) - { - if (Uri.TryCreate(GetStandardClaim(UdapConstants.RegistrationDocumentValues.ClientUri), UriKind.Absolute, out var value )) - { - _clientUri = value; - } - } - return _clientUri; - } - set - { - _clientUri = value; - if (value != null) this[UdapConstants.RegistrationDocumentValues.ClientUri] = value; - } - } + + // + // Dropping this for now. + // If I bring it back read and follow instructions here: https://www.rfc-editor.org/rfc/rfc7591#section-5 + // + // /// + // /// Web page providing information about the client. + // /// See + // /// + // [JsonPropertyName(UdapConstants.RegistrationDocumentValues.ClientUri)] + // public Uri? ClientUri { + // get + // { + // if (_clientUri == null) + // { + // if (Uri.TryCreate(GetStandardClaim(UdapConstants.RegistrationDocumentValues.ClientUri), UriKind.Absolute, out var value )) + // { + // _clientUri = value; + // } + // } + // return _clientUri; + // } + // set + // { + // _clientUri = value; + // if (value != null) this[UdapConstants.RegistrationDocumentValues.ClientUri] = value; + // } + // } /// @@ -499,6 +503,16 @@ public string? Scope } } + public string? GetError() + { + return GetStandardClaim("error"); + } + + public string? GetErrorDescription() + { + return GetStandardClaim("error_description"); + } + /// /// Similar to . /// Adds a number of to the . @@ -529,13 +543,8 @@ public void AddClaims(IEnumerable claims) foreach (Claim claim in claims) { - if (claim == null) - { - continue; - } - - string jsonClaimType = claim.Type; - object jsonClaimValue = claim.ValueType.Equals(ClaimValueTypes.String) ? claim.Value : GetClaimValueUsingValueType(claim); + var jsonClaimType = claim.Type; + var jsonClaimValue = claim.ValueType.Equals(ClaimValueTypes.String) ? claim.Value : GetClaimValueUsingValueType(claim); // If there is an existing value, append to it. // What to do if the 'ClaimValueType' is not the same. @@ -695,11 +704,6 @@ internal static object GetClaimValueUsingValueType(Claim claim) if (claim.ValueType == JsonClaimValueTypes.JsonNull) return string.Empty; - if (claim.Value == null) - { - return string.Empty; - } - if (claim.ValueType == JsonClaimValueTypes.Json) return JsonNode.Parse(claim.Value)!; diff --git a/Udap.Model/Registration/UdapDynamicClientRegistrationErrors.cs b/Udap.Model/Registration/UdapDynamicClientRegistrationErrors.cs index dd6a3c41..86a7e3ac 100644 --- a/Udap.Model/Registration/UdapDynamicClientRegistrationErrors.cs +++ b/Udap.Model/Registration/UdapDynamicClientRegistrationErrors.cs @@ -38,8 +38,9 @@ public static class UdapDynamicClientRegistrationErrorDescriptions public const string ClientNameMissing = "client_name is missing"; public const string LogoMissing = "logo_uri is missing"; public const string LogoInvalidUri = "logo_uri is not a valid uri"; - public const string LogoInvalidFileType = "logo_uri is not a valid file type"; + public const string LogoInvalidContentType = "logo_uri is not a valid content-type"; public const string LogoInvalidScheme = "logo_uri is not a valid https schema"; + public const string LogoCannotBeResolved = "logo_uri cannot be resolved"; public const string GrantTypeMissing = "grant_types is missing"; public const string ResponseTypesMissing = "invalid_client_metadata response_types is missing"; diff --git a/Udap.Model/Statement/SignedSoftwareStatementBuilder.cs b/Udap.Model/Statement/SignedSoftwareStatementBuilder.cs index 78a092df..f17a1c1c 100644 --- a/Udap.Model/Statement/SignedSoftwareStatementBuilder.cs +++ b/Udap.Model/Statement/SignedSoftwareStatementBuilder.cs @@ -9,13 +9,11 @@ using System; using System.IdentityModel.Tokens.Jwt; -using System.Runtime.ConstrainedExecution; using System.Runtime.InteropServices; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using Microsoft.IdentityModel.JsonWebTokens; using Microsoft.IdentityModel.Tokens; -using Org.BouncyCastle.Math.EC; using Udap.Model.Registration; using ECCurve = System.Security.Cryptography.ECCurve; @@ -94,15 +92,14 @@ public string BuildECDSA(string? algorithm = UdapConstants.SupportedAlgorithm.ES // https://github.com/dotnet/runtime/issues/77590#issuecomment-1325896560 // https://stackoverflow.com/a/57330499/6115838 // - byte[] encryptedPrivKeyBytes = key.ExportEncryptedPkcs8PrivateKey( + var encryptedPrivateKeyBytes = key?.ExportEncryptedPkcs8PrivateKey( "ILikePasswords", new PbeParameters( PbeEncryptionAlgorithm.Aes256Cbc, HashAlgorithmName.SHA256, iterationCount: 100_000)); - ecdsa.ImportEncryptedPkcs8PrivateKey("ILikePasswords".AsSpan(), encryptedPrivKeyBytes.AsSpan(), - out int bytesRead); + ecdsa.ImportEncryptedPkcs8PrivateKey("ILikePasswords".AsSpan(), encryptedPrivateKeyBytes.AsSpan(), out _); } else { diff --git a/Udap.Model/Udap.Model.csproj b/Udap.Model/Udap.Model.csproj index 51db96cd..cd2d11e2 100644 --- a/Udap.Model/Udap.Model.csproj +++ b/Udap.Model/Udap.Model.csproj @@ -27,9 +27,9 @@ - + - + @@ -44,4 +44,21 @@ + + + + ..\..\..\..\DuendeSoftware\IdentityServer\src\Storage\bin\Debug\net7.0\Duende.IdentityServer.Storage.dll + + + + + + + + + + + + + diff --git a/Udap.Model/UdapConstants.cs b/Udap.Model/UdapConstants.cs index d4ed3715..8a3400ca 100644 --- a/Udap.Model/UdapConstants.cs +++ b/Udap.Model/UdapConstants.cs @@ -7,8 +7,6 @@ // */ #endregion -using Udap.Model.UdapAuthenticationExtensions; - namespace Udap.Model; /// diff --git a/Udap.Server/Configuration/DependencyInjection/Additional.cs b/Udap.Server/Configuration/DependencyInjection/Additional.cs index 6ad3f0e9..fbbbb0db 100644 --- a/Udap.Server/Configuration/DependencyInjection/Additional.cs +++ b/Udap.Server/Configuration/DependencyInjection/Additional.cs @@ -7,7 +7,9 @@ // */ #endregion +using Duende.IdentityServer.Hosting.DynamicProviders; using Duende.IdentityServer.Services; +using Duende.IdentityServer.Stores; using Duende.IdentityServer.Validation; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; diff --git a/Udap.Server/Configuration/DependencyInjection/UdapBuilderExtensions/UdapCore.cs b/Udap.Server/Configuration/DependencyInjection/UdapBuilderExtensions/UdapCore.cs index 1a88fa1f..80673fed 100644 --- a/Udap.Server/Configuration/DependencyInjection/UdapBuilderExtensions/UdapCore.cs +++ b/Udap.Server/Configuration/DependencyInjection/UdapBuilderExtensions/UdapCore.cs @@ -15,20 +15,26 @@ // using Duende.IdentityServer.Configuration; using Duende.IdentityServer.Hosting; +using Duende.IdentityServer.Models; using Duende.IdentityServer.ResponseHandling; using IdentityModel; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using Udap.Client.Client; using Udap.Common; using Udap.Common.Certificates; using Udap.Common.Extensions; +using Udap.Model; using Udap.Server; using Udap.Server.Configuration.DependencyInjection; using Udap.Server.DbContexts; +using Udap.Server.Hosting.DynamicProviders.Oidc; using Udap.Server.Options; using Udap.Server.ResponseHandling; +using Udap.Server.Security.Authentication.TieredOAuth; using Udap.Server.Stores; using Udap.Server.Validation; using Constants = Udap.Server.Constants; @@ -95,7 +101,7 @@ public static IUdapServiceBuilder AddUdapConfigurationStore( public static IUdapServiceBuilder AddSmartV2Expander(this IUdapServiceBuilder builder) { - builder.Services.AddScoped(); + builder.Services.AddScoped(); return builder; } @@ -118,4 +124,4 @@ public static IUdapServiceBuilder AddPrivateFileStore(this IUdapServiceBuilder b return builder; } -} +} \ No newline at end of file diff --git a/Udap.Server/Configuration/ServerSettings.cs b/Udap.Server/Configuration/ServerSettings.cs index 37e36afa..cabd7b25 100644 --- a/Udap.Server/Configuration/ServerSettings.cs +++ b/Udap.Server/Configuration/ServerSettings.cs @@ -34,9 +34,7 @@ public class ServerSettings /// [JsonPropertyName("ForceStateParamOnAuthorizationCode")] public bool ForceStateParamOnAuthorizationCode { get; set; } = false; - - public ICollection? IdPMappings { get; set; } - + [JsonPropertyName("LogoRequired")] public bool LogoRequired { get; set; } = true; @@ -49,11 +47,6 @@ public class ServerSettings public bool AlwaysIncludeUserClaimsInIdToken { get; set; } } -public class IdPMapping -{ - public string? Scheme { get; set; } - public string? IdpBaseUrl { get; set; } -} public enum ServerSupport { diff --git a/Udap.Server/Entities/TieredClient.cs b/Udap.Server/Entities/TieredClient.cs index 3cbc9328..a28ad016 100644 --- a/Udap.Server/Entities/TieredClient.cs +++ b/Udap.Server/Entities/TieredClient.cs @@ -23,4 +23,6 @@ public class TieredClient public int CommunityId { get; set; } public bool Enabled { get; set; } + + public string TokenEndpoint { get; set; } } \ No newline at end of file diff --git a/Udap.Server/Hosting/DynamicProviders/Oidc/IdentityServerBuilderExtensions.cs b/Udap.Server/Hosting/DynamicProviders/Oidc/IdentityServerBuilderExtensions.cs new file mode 100644 index 00000000..80683334 --- /dev/null +++ b/Udap.Server/Hosting/DynamicProviders/Oidc/IdentityServerBuilderExtensions.cs @@ -0,0 +1,28 @@ +#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 Duende.IdentityServer.Models; +using Microsoft.Extensions.DependencyInjection; +using Udap.Server.Hosting.DynamicProviders.Store; + +namespace Udap.Server.Hosting.DynamicProviders.Oidc; +public static class IdentityServerBuilderExtensions +{ + + + public static IIdentityServerBuilder AddInMemorIdentityProviders( + this IIdentityServerBuilder builder, IEnumerable identityProviders) + { + builder.Services.AddSingleton(identityProviders); + builder.AddIdentityProviderStore(); + + return builder; + } + +} diff --git a/Udap.Server/Hosting/DynamicProviders/Store/UdapInMemoryIdentityProviderStore.cs b/Udap.Server/Hosting/DynamicProviders/Store/UdapInMemoryIdentityProviderStore.cs new file mode 100644 index 00000000..cb34edc5 --- /dev/null +++ b/Udap.Server/Hosting/DynamicProviders/Store/UdapInMemoryIdentityProviderStore.cs @@ -0,0 +1,45 @@ +#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 Duende.IdentityServer.Models; +using Duende.IdentityServer.Stores; +using Udap.Common; + +namespace Udap.Server.Hosting.DynamicProviders.Store; +internal class UdapInMemoryIdentityProviderStore : IIdentityProviderStore +{ + private readonly IEnumerable _providers; + + public UdapInMemoryIdentityProviderStore(IEnumerable providers) + { + _providers = providers; + } + + public Task> GetAllSchemeNamesAsync() + { + using var activity = Tracing.StoreActivitySource.StartActivity("UdapInMemoryIdentityProviderStore.GetAllSchemeNames"); + + var items = _providers.Select(x => new IdentityProviderName + { + Enabled = x.Enabled, + DisplayName = x.DisplayName, + Scheme = x.Scheme + }); + + return Task.FromResult(items); + } + + public Task GetBySchemeAsync(string scheme) + { + using var activity = Tracing.StoreActivitySource.StartActivity("UdapInMemoryIdentityProviderStore.GetByScheme"); + + var item = _providers.FirstOrDefault(x => x.Scheme == scheme); + return Task.FromResult(item); + } +} \ No newline at end of file diff --git a/Udap.Server/Registration/UdapDynamicClientRegistrationValidationResult.cs b/Udap.Server/Registration/UdapDynamicClientRegistrationValidationResult.cs index bc655674..7ef0c3a9 100644 --- a/Udap.Server/Registration/UdapDynamicClientRegistrationValidationResult.cs +++ b/Udap.Server/Registration/UdapDynamicClientRegistrationValidationResult.cs @@ -8,6 +8,7 @@ #endregion using Udap.Model.Registration; +using Udap.Util.Extensions; namespace Udap.Server.Registration; diff --git a/Udap.Server/Registration/UdapDynamicClientRegistrationValidator.cs b/Udap.Server/Registration/UdapDynamicClientRegistrationValidator.cs index a1a83fc8..fc6b3196 100644 --- a/Udap.Server/Registration/UdapDynamicClientRegistrationValidator.cs +++ b/Udap.Server/Registration/UdapDynamicClientRegistrationValidator.cs @@ -16,16 +16,21 @@ // using System.IdentityModel.Tokens.Jwt; +using System.Net.Http; +using System.Net.Mime; +using System.Reflection.Metadata; using System.Security.Cryptography.X509Certificates; using System.Text; using Duende.IdentityServer.Models; using Duende.IdentityServer.Services; +using Duende.IdentityServer.Stores; using IdentityModel; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.Extensions.Logging; using Microsoft.IdentityModel.JsonWebTokens; using Microsoft.IdentityModel.Tokens; +using Udap.Client.Client; using Udap.Common; using Udap.Common.Certificates; using Udap.Common.Extensions; @@ -44,27 +49,33 @@ namespace Udap.Server.Registration; public class UdapDynamicClientRegistrationValidator : IUdapDynamicClientRegistrationValidator { private readonly TrustChainValidator _trustChainValidator; + private readonly HttpClient _httpClient; private readonly IReplayCache _replayCache; private readonly ILogger _logger; private readonly ServerSettings _serverSettings; private readonly IHttpContextAccessor _httpContextAccessor; private readonly IScopeExpander _scopeExpander; + private readonly IResourceStore _resourceStore; private const string Purpose = nameof(UdapDynamicClientRegistrationValidator); public UdapDynamicClientRegistrationValidator( TrustChainValidator trustChainValidator, + HttpClient httpClient, IReplayCache replayCache, ServerSettings serverSettings, IHttpContextAccessor httpContextAccessor, IScopeExpander scopeExpander, + IResourceStore resourceStore, //TODO use CachingResourceStore ILogger logger) { _trustChainValidator = trustChainValidator; + _httpClient = httpClient; _replayCache = replayCache; _serverSettings = serverSettings; _httpContextAccessor = httpContextAccessor; _scopeExpander = scopeExpander; + _resourceStore = resourceStore; _logger = logger; } @@ -367,7 +378,8 @@ IEnumerable anchors { if (_serverSettings.LogoRequired) { - if ( ! ValidateLogoUri(document, out UdapDynamicClientRegistrationValidationResult? errorResult)) + var (successFlag, errorResult) = await ValidateLogoUri(document); + if (!successFlag) { return errorResult!; } @@ -481,13 +493,31 @@ IEnumerable anchors { var scopes = document.Scope.Split(' ', StringSplitOptions.RemoveEmptyEntries); // todo: ideally scope names get checked against configuration store? + + var resources = await _resourceStore.GetAllEnabledResourcesAsync(); + var expandedScopes = _scopeExpander.Expand(scopes).ToList(); + var explodedScopes = _scopeExpander.WildCardExpand(expandedScopes, resources.ApiScopes.Select(a => a.Name).ToList()).ToList(); + var allowedApiScopes = resources.ApiScopes.Where(s => explodedScopes.Contains(s.Name)); - var expandedScopes = _scopeExpander.Expand(scopes); + foreach (var scope in allowedApiScopes) + { + client?.AllowedScopes.Add(scope.Name); + } + + var allowedResourceScopes = resources.IdentityResources.Where(s => explodedScopes.Contains(s.Name)); - foreach (var scope in expandedScopes) + foreach (var scope in allowedResourceScopes.Where(s => s.Enabled).Select(s => s.Name)) { client?.AllowedScopes.Add(scope); } + + // + // Present scopes in aggregate form + // + if (client?.AllowedScopes != null) + { + document.Scope = _scopeExpander.Aggregate(client.AllowedScopes).OrderBy(s => s).ToSpaceSeparatedString(); + } } @@ -503,10 +533,9 @@ IEnumerable anchors return await Task.FromResult(new UdapDynamicClientRegistrationValidationResult(client, document)); } - public bool ValidateLogoUri(UdapDynamicClientRegistrationDocument document, - out UdapDynamicClientRegistrationValidationResult? errorResult) + public async Task<(bool, UdapDynamicClientRegistrationValidationResult?)> ValidateLogoUri(UdapDynamicClientRegistrationDocument document) { - errorResult = null; + UdapDynamicClientRegistrationValidationResult? errorResult; if (string.IsNullOrEmpty(document.LogoUri)) { @@ -514,32 +543,44 @@ public bool ValidateLogoUri(UdapDynamicClientRegistrationDocument document, UdapDynamicClientRegistrationErrors.InvalidClientMetadata, UdapDynamicClientRegistrationErrorDescriptions.LogoMissing); - return false; + return (false, errorResult); } if (Uri.TryCreate(document.LogoUri, UriKind.Absolute, out var logoUri)) { - if (!logoUri.OriginalString.EndsWith("png", StringComparison.OrdinalIgnoreCase) && - !logoUri.OriginalString.EndsWith("jpg", StringComparison.OrdinalIgnoreCase) && - !logoUri.OriginalString.EndsWith("gif", StringComparison.OrdinalIgnoreCase)) + if (!logoUri.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase)) { errorResult = new UdapDynamicClientRegistrationValidationResult( UdapDynamicClientRegistrationErrors.InvalidClientMetadata, - UdapDynamicClientRegistrationErrorDescriptions.LogoInvalidFileType); + UdapDynamicClientRegistrationErrorDescriptions.LogoInvalidScheme); - return false; + return (false, errorResult); } + + var response = await _httpClient.GetAsync(logoUri.OriginalString); + response.Content.Headers.TryGetValues("Content-Type", out var contentTypes); + var contentType = contentTypes?.FirstOrDefault(); - if (!logoUri.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase)) + if (response.StatusCode != System.Net.HttpStatusCode.OK) { errorResult = new UdapDynamicClientRegistrationValidationResult( - UdapDynamicClientRegistrationErrors.InvalidClientMetadata, - UdapDynamicClientRegistrationErrorDescriptions.LogoInvalidScheme); + UdapDynamicClientRegistrationErrors.InvalidClientMetadata, + UdapDynamicClientRegistrationErrorDescriptions.LogoCannotBeResolved); - return false; + return (false, errorResult); } + + if (contentType == null || + !contentType.Equals("image/png", StringComparison.OrdinalIgnoreCase) && + !contentType.Equals(MediaTypeNames.Image.Jpeg, StringComparison.OrdinalIgnoreCase) && + !contentType.Equals(MediaTypeNames.Image.Gif, StringComparison.OrdinalIgnoreCase)) + { + errorResult = new UdapDynamicClientRegistrationValidationResult( + UdapDynamicClientRegistrationErrors.InvalidClientMetadata, + UdapDynamicClientRegistrationErrorDescriptions.LogoInvalidContentType); - + return (false, errorResult); + } } else { @@ -547,10 +588,10 @@ public bool ValidateLogoUri(UdapDynamicClientRegistrationDocument document, UdapDynamicClientRegistrationErrors.InvalidClientMetadata, UdapDynamicClientRegistrationErrorDescriptions.LogoInvalidUri); - return false; + return (false, errorResult); } - return true; + return (true, null); } public async Task ValidateJti( diff --git a/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthAuthenticationDefaults.cs b/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthAuthenticationDefaults.cs index 4747b794..c5602992 100644 --- a/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthAuthenticationDefaults.cs +++ b/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthAuthenticationDefaults.cs @@ -21,11 +21,7 @@ public static class TieredOAuthAuthenticationDefaults /// /// Default value for . /// - public static readonly string DisplayName = "UDAP Tiered OAuth (DOTNET-Provider1)"; + public static readonly string DisplayName = "Launch Tiered OAuth"; - public static readonly string CallbackPath = "/signin-tieredoauth"; - - // public static readonly string AuthorizationEndpoint = "https://localhost:5055/connect/authorize"; - // - // public static readonly string TokenEndpoint = "https://localhost:5055/connect/token"; + public static readonly string CallbackPath = "/federation/udap-tiered/signin"; } \ No newline at end of file diff --git a/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthAuthenticationExtensions.cs b/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthAuthenticationExtensions.cs index 53af7340..86a65967 100644 --- a/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthAuthenticationExtensions.cs +++ b/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthAuthenticationExtensions.cs @@ -78,7 +78,7 @@ public static AuthenticationBuilder AddTieredOAuth( Action configuration) { - builder.Services.AddTransient(); + builder.Services.TryAddTransient(); builder.Services.AddHttpClient().AddHttpMessageHandler(); builder.Services.TryAddSingleton(sp => @@ -91,7 +91,7 @@ public static AuthenticationBuilder AddTieredOAuth( return handler; }); - + builder.Services.TryAddSingleton, TieredOAuthPostConfigureOptions>(); return builder.AddOAuth(scheme, caption, configuration); } diff --git a/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthAuthenticationHandler.cs b/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthAuthenticationHandler.cs index 518d5561..3fdcd748 100644 --- a/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthAuthenticationHandler.cs +++ b/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthAuthenticationHandler.cs @@ -11,19 +11,17 @@ using System.Globalization; using System.IdentityModel.Tokens.Jwt; 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.Models; -using Duende.IdentityServer.Stores; using IdentityModel; using IdentityModel.Client; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.OAuth; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.WebUtilities; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; @@ -31,21 +29,20 @@ using Microsoft.IdentityModel.Tokens; using Udap.Client.Client; using Udap.Common.Certificates; -using Udap.Common.Extensions; using Udap.Common.Models; +using Udap.Model; using Udap.Model.Access; -using Udap.Model.Registration; using Udap.Server.Storage.Stores; using Udap.Util.Extensions; -using static IdentityModel.ClaimComparer; namespace Udap.Server.Security.Authentication.TieredOAuth; + public class TieredOAuthAuthenticationHandler : OAuthHandler { private readonly IUdapClient _udapClient; private readonly IPrivateCertificateStore _certificateStore; - private readonly IServiceScopeFactory _scopeFactory; + private readonly IUdapClientRegistrationStore _udapClientRegistrationStore; /// /// Initializes a new instance of . @@ -58,12 +55,13 @@ public TieredOAuthAuthenticationHandler( ISystemClock clock, IUdapClient udapClient, IPrivateCertificateStore certificateStore, - IServiceScopeFactory scopeFactory) : + IUdapClientRegistrationStore udapClientRegistrationStore + ) : base(options, logger, encoder, clock) { _udapClient = udapClient; _certificateStore = certificateStore; - _scopeFactory = scopeFactory; + _udapClientRegistrationStore = udapClientRegistrationStore; } /// Constructs the OAuth challenge url. @@ -83,10 +81,39 @@ protected override string BuildChallengeUrl(AuthenticationProperties properties, var state = Options.StateDataFormat.Protect(properties); queryStrings.Add("state", state); - var authorizationEndpoint = QueryHelpers.AddQueryString(Options.AuthorizationEndpoint, queryStrings!); - - return authorizationEndpoint; + + // Static configured Options + // if (!Options.AuthorizationEndpoint.IsNullOrEmpty()) + // { + // return QueryHelpers.AddQueryString(Options.AuthorizationEndpoint, queryStrings!); + // } + + var authEndpoint = properties.Parameters[UdapConstants.Discovery.AuthorizationEndpoint] as string ?? + throw new InvalidOperationException("Missing IdP authorization endpoint."); + + var community = properties.GetParameter(UdapConstants.Community); + if (!community.IsNullOrEmpty()) + { + queryStrings.Add(UdapConstants.Community, community!); + } + + var tokenEndpoint = properties.GetParameter(UdapConstants.Discovery.TokenEndpoint); + + if (!tokenEndpoint.IsNullOrEmpty()) + { + Options.TokenEndpoint = tokenEndpoint!; + } + + var idpBaseUrl = properties.GetParameter("idpBaseUrl"); + + if (!idpBaseUrl.IsNullOrEmpty()) + { + Options.IdPBaseUrl = idpBaseUrl; + } + + // Dynamic options + return QueryHelpers.AddQueryString(authEndpoint, queryStrings!); } @@ -225,11 +252,14 @@ protected override async Task HandleRemoteAuthenticateAsync var validationParameters = Options.TokenValidationParameters.Clone(); + var clientId = properties.Items["client_id"] ?? throw new InvalidOperationException($"ClientId not found in properties"); + var tieredClient = await _udapClientRegistrationStore.FindTieredClientById(clientId) ?? throw new InvalidOperationException($"ClientId not found in registration store: {clientId}"); + // TODO: pre installed keys check? var request = new DiscoveryDocumentRequest { - Address = Options.IdPBaseUrl, + Address = tieredClient.IdPBaseUrl, Policy = new IdentityModel.Client.DiscoveryPolicy() { //TODO: Promote to TieredOAuthOptions. Maybe even injectable for advanced use cases. @@ -240,7 +270,7 @@ protected override async Task HandleRemoteAuthenticateAsync var keys = await _udapClient.ResolveJwtKeys(request); validationParameters.IssuerSigningKeys = keys; - var tokenEndpointUser = ValidateToken(idToken, properties, validationParameters, out var tokenEndpointJwt); + var tokenEndpointUser = ValidateToken(idToken, tieredClient.IdPBaseUrl, clientId, validationParameters, out var tokenEndpointJwt); // nonce = tokenEndpointJwt.Payload.Nonce; // if (!string.IsNullOrEmpty(nonce)) @@ -272,7 +302,8 @@ protected override async Task HandleRemoteAuthenticateAsync // Note this modifies properties if Options.UseTokenLifetime private ClaimsPrincipal ValidateToken( string idToken, - AuthenticationProperties properties, + string idpBaseUrl, + string clientId, TokenValidationParameters validationParameters, out JwtSecurityToken jwt) { @@ -293,8 +324,7 @@ private ClaimsPrincipal ValidateToken( // no need to validate signature when token is received using "code flow" as per spec // [http://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation]. - validationParameters.ValidIssuer = Options.IdPBaseUrl; - properties.Items.TryGetValue("client_id", out var clientId); + validationParameters.ValidIssuers = new List{ idpBaseUrl , idpBaseUrl.TrimEnd('/')}; validationParameters.ValidAudience = clientId; validationParameters.ValidateIssuerSigningKey = false; @@ -380,28 +410,33 @@ protected override async Task ExchangeCodeAsync([NotNull] OA var originalRequestParams = HttpUtility.ParseQueryString(context.Properties.Items["returnUrl"] ?? "~/"); var idp = (originalRequestParams.GetValues("idp") ?? throw new InvalidOperationException()).Last(); + var idpUri = new Uri(idp); + var communityParam = (HttpUtility.ParseQueryString(idpUri.Query).GetValues("community") ?? Array.Empty()).LastOrDefault(); + var clientId = context.Properties.Items["client_id"]; var resourceHolderRedirectUrl = - $"{Context.Request.Scheme}://{Context.Request.Host}{Context.Request.PathBase}{Options.CallbackPath}"; + $"{Context.Request.Scheme}{Uri.SchemeDelimiter}{Context.Request.Host}{Context.Request.PathBase}{Options.CallbackPath}"; var requestParams = Context.Request.Query; var code = requestParams["code"]; - - using var serviceScope = _scopeFactory.CreateScope(); - var clientStore = serviceScope.ServiceProvider.GetRequiredService(); - var idpClient = await clientStore.FindTieredClientById(clientId); - var idpClientId = idpClient.ClientId; - + var tieredClient = await _udapClientRegistrationStore.FindTieredClientById(clientId) ?? throw new InvalidOperationException($"ClientId not found: {clientId}"); + var tieredClientId = tieredClient.ClientId; + await _certificateStore.Resolve(); // Sign request for token var tokenRequestBuilder = AccessTokenRequestForAuthorizationCodeBuilder.Create( - idpClientId, - Options.TokenEndpoint, - _certificateStore.IssuedCertificates.Where(ic => ic.IdPBaseUrl == idp) + tieredClientId, + tieredClient.TokenEndpoint, + communityParam == null + ? + _certificateStore.IssuedCertificates.First().Certificate + : + _certificateStore.IssuedCertificates.Where(ic => ic.Community == communityParam) //TODO: multiple certs or latest cert? - .Select(ic => ic.Certificate).First(), + .Select(ic => ic.Certificate).First(), + resourceHolderRedirectUrl, code); @@ -416,22 +451,35 @@ protected override async Task HandleChallengeAsync(AuthenticationProperties prop { var requestParams = HttpUtility.ParseQueryString(properties.Items["returnUrl"] ?? "~/"); - var idp = (requestParams.GetValues("idp") ?? throw new InvalidOperationException()).Last(); + var idpParam = (requestParams.GetValues("idp") ?? throw new InvalidOperationException()).Last(); var scope = (requestParams.GetValues("scope") ?? throw new InvalidOperationException()).First(); var clientRedirectUrl = (requestParams.GetValues("redirect_uri") ?? throw new InvalidOperationException()).Last(); var updateRegistration = requestParams.GetValues("update_registration")?.Last(); // Validate idp Server; - var community = idp.GetCommunityFromQueryParams(); - + var idpUri = new Uri(idpParam); + var communityParam = (HttpUtility.ParseQueryString(idpUri.Query).GetValues("community") ?? Array.Empty()).LastOrDefault(); + var idp = idpUri.OriginalString; + if (communityParam != null) + { + if (idp.Contains($":{idpUri.Port}")) + { + idp = $"{idpUri.Scheme}{Uri.SchemeDelimiter}{idpUri.Host}:{idpUri.Port}{idpUri.LocalPath}"; + } + else + { + idp = $"{idpUri.Scheme}{Uri.SchemeDelimiter}{idpUri.Host}{idpUri.LocalPath}"; + } + } + _udapClient.Problem += element => properties.Parameters.Add("Problem", element.ChainElementStatus.Summarize(TrustChainValidator.DefaultProblemFlags)); _udapClient.Untrusted += certificate2 => properties.Parameters.Add("Untrusted", certificate2.Subject); _udapClient.TokenError += message => properties.Parameters.Add("TokenError", message); - var response = await _udapClient.ValidateResource(idp, community); + var response = await _udapClient.ValidateResource(idp, communityParam); var resourceHolderRedirectUrl = - $"{Context.Request.Scheme}://{Context.Request.Host}{Options.CallbackPath}"; + $"{Context.Request.Scheme}{Uri.SchemeDelimiter}{Context.Request.Host}{Options.CallbackPath}"; if (response.IsError) { @@ -463,9 +511,7 @@ protected override async Task HandleChallengeAsync(AuthenticationProperties prop // if not registered with IdP, then register. // - using var serviceScope = _scopeFactory.CreateScope(); - var clientStore = serviceScope.ServiceProvider.GetRequiredService(); - var idpClient = await clientStore.FindTieredClientById(idp); + var idpClient = await _udapClientRegistrationStore.FindTieredClientById(idp); var idpClientId = null as string; @@ -479,17 +525,8 @@ protected override async Task HandleChallengeAsync(AuthenticationProperties prop if (idpClient == null || !idpClient.Enabled || updateRegistration == "true") { await _certificateStore.Resolve(); - - - - // What does this code even accomplish? Shouldn't it look for more than the first community? - // Maybe I am still tied to one community. Lots more work here. - - var communityName = _certificateStore.IssuedCertificates.First().Community; - var registrationStore = serviceScope.ServiceProvider.GetRequiredService(); - - var communityId = - await registrationStore.GetCommunityId(communityName, Context.RequestAborted); + var communityName = communityParam ?? _certificateStore.IssuedCertificates.First().Community; //TODO: query by + var communityId = await _udapClientRegistrationStore.GetCommunityId(communityName, Context.RequestAborted); if (communityId == null) { @@ -499,18 +536,27 @@ protected override async Task HandleChallengeAsync(AuthenticationProperties prop //Todo: return strategy? return; } - - - //TODO: RegisterClient should be typed to the two builders - // UdapDcrBuilderForAuthorizationCode or UdapDcrBuilderForClientCredentials var document = await _udapClient.RegisterTieredClient( resourceHolderRedirectUrl, - _certificateStore.IssuedCertificates.Where(ic => ic.IdPBaseUrl == idp) - .Select(ic => ic.Certificate), + + communityParam == null + ? + new List(){ _certificateStore.IssuedCertificates.First().Certificate } + : + _certificateStore.IssuedCertificates.Where(ic => ic.Community == communityParam) + .Select(ic => ic.Certificate), + OptionsMonitor.CurrentValue.Scope.ToSpaceSeparatedString(), Context.RequestAborted); + if (document.GetError() != null) + { + Logger.LogWarning(document.GetError() + ": " + document.GetErrorDescription()); + await base.HandleChallengeAsync(properties); + return; + } + if (idpClient == null) { idpClientId = document.ClientId; @@ -528,12 +574,15 @@ protected override async Task HandleChallengeAsync(AuthenticationProperties prop RedirectUri = clientRedirectUrl, ClientUriSan = publicCert.GetSubjectAltNames().First().Item2, //TODO: can a AuthServer register multiple times per community? CommunityId = communityId.Value, - Enabled = true + Enabled = true, + TokenEndpoint = properties.Parameters[UdapConstants.Discovery.TokenEndpoint] as string ?? throw new InvalidOperationException("Missing IdP token endpoint."), }; - await registrationStore.UpsertTieredClient(tieredClient, Context.RequestAborted); + await _udapClientRegistrationStore.UpsertTieredClient(tieredClient, Context.RequestAborted); } + + properties.SetString("client_id", idpClientId); await base.HandleChallengeAsync(properties); diff --git a/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthAuthenticationOptions.cs b/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthAuthenticationOptions.cs index a7df3c8c..1f53d9a2 100644 --- a/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthAuthenticationOptions.cs +++ b/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthAuthenticationOptions.cs @@ -11,30 +11,39 @@ using Microsoft.IdentityModel.Tokens; using System.IdentityModel.Tokens.Jwt; using IdentityModel; -using Udap.Model; namespace Udap.Server.Security.Authentication.TieredOAuth; -public class TieredOAuthAuthenticationOptions : OAuthOptions{ +public class TieredOAuthAuthenticationOptions : OAuthOptions +{ private readonly JwtSecurityTokenHandler _defaultHandler = new JwtSecurityTokenHandler(); public TieredOAuthAuthenticationOptions() { - CallbackPath = TieredOAuthAuthenticationDefaults.CallbackPath; - ClientId = "dynamic"; - ClientSecret = "signed metadata"; - // AuthorizationEndpoint = TieredOAuthAuthenticationDefaults.AuthorizationEndpoint; - // TokenEndpoint = TieredOAuthAuthenticationDefaults.TokenEndpoint; SignInScheme = TieredOAuthAuthenticationDefaults.AuthenticationScheme; - - // TODO: configurable. + + // TODO: configurable for the non-dynamic AddTieredOAuthForTests call. Scope.Add(OidcConstants.StandardScopes.OpenId); - // Scope.Add(UdapConstants.StandardScopes.FhirUser); Scope.Add(OidcConstants.StandardScopes.Email); Scope.Add(OidcConstants.StandardScopes.Profile); SecurityTokenValidator = _defaultHandler; + + // + // Properties below are required to survive Microsoft.AspNetCore.Authentication.RemoteAuthenticationOptions.Validate(String scheme) + // + // AuthorizationEndpoint and TokenEndpoint are placed them in the AuthenticationProperties.Parameters + // and set during the GET /externallogin/challenge + // + // ClientSecret is not used + // ClientId is set after dynamic registration + // + AuthorizationEndpoint = "/connect/authorize"; + TokenEndpoint = "/connect/token"; + ClientSecret = "signed metadata"; + ClientId = "temporary"; + CallbackPath = TieredOAuthAuthenticationDefaults.CallbackPath; } /// @@ -54,4 +63,6 @@ public TieredOAuthAuthenticationOptions() /// /// public string IdPBaseUrl { get; set; } + + public string Community { get; set; } } \ No newline at end of file diff --git a/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthHelpers.cs b/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthHelpers.cs new file mode 100644 index 00000000..819ceade --- /dev/null +++ b/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthHelpers.cs @@ -0,0 +1,86 @@ +#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.Web; +using Duende.IdentityServer.Services; +using IdentityModel; +using IdentityModel.Client; +using Microsoft.AspNetCore.Authentication; +using Udap.Client.Client; +using Udap.Model; + +namespace Udap.Server.Security.Authentication.TieredOAuth; +public static class TieredOAuthHelpers +{ + public static async Task BuildDynamicTieredOAuthOptions( + IIdentityServerInteractionService interactionService, + IUdapClient udapClient, + string scheme, + string redirectUri, + string returnUrl) + { + if (interactionService.IsValidReturnUrl(returnUrl) == false) + { + throw new Exception("invalid return URL"); + } + + var props = new AuthenticationProperties + { + RedirectUri = redirectUri, + + Items = + { + { "returnUrl", returnUrl }, + { "scheme", scheme }, + } + }; + + + var originalRequestParams = HttpUtility.ParseQueryString(returnUrl); + var idp = (originalRequestParams.GetValues("idp") ?? throw new InvalidOperationException()).Last(); + + var parts = idp.Split(new[] { '?' }, StringSplitOptions.RemoveEmptyEntries); + + if (parts.Length > 1) + { + props.Parameters.Add(UdapConstants.Community, parts[1]); + } + + var idpUri = new Uri(idp); + string idpBaseUrl; + + if (idp.Contains($":{idpUri.Port}")) + { + idpBaseUrl = $"{idpUri.Scheme}{Uri.SchemeDelimiter}{idpUri.Host}:{idpUri.Port}{idpUri.LocalPath}"; + } + else + { + idpBaseUrl = $"{idpUri.Scheme}{Uri.SchemeDelimiter}{idpUri.Host}{idpUri.LocalPath}"; + } + + var request = new DiscoveryDocumentRequest + { + Address = idpBaseUrl, + Policy = new IdentityModel.Client.DiscoveryPolicy() + { + EndpointValidationExcludeList = new List { OidcConstants.Discovery.RegistrationEndpoint } + } + }; + + var openIdConfig = await udapClient.ResolveOpenIdConfig(request); + + // TODO: Properties will be protected in state in the BuildchallengeUrl. Need to trim out some of these + // during the protect process. + props.Parameters.Add(UdapConstants.Discovery.AuthorizationEndpoint, openIdConfig.AuthorizeEndpoint); + props.Parameters.Add(UdapConstants.Discovery.TokenEndpoint, openIdConfig.TokenEndpoint); + props.Parameters.Add("idpBaseUrl", idpBaseUrl.TrimEnd('/')); + + return props; + } +} diff --git a/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthPostConfigureOptions.cs b/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthPostConfigureOptions.cs index e1e2413b..12ce9429 100644 --- a/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthPostConfigureOptions.cs +++ b/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthPostConfigureOptions.cs @@ -7,6 +7,8 @@ // */ #endregion +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.DataProtection; using Microsoft.Extensions.Options; using Udap.Client.Client; @@ -16,18 +18,21 @@ public class TieredOAuthPostConfigureOptions : IPostConfigureOptions /// Initializes a new instance of the class. /// /// - public TieredOAuthPostConfigureOptions(UdapClientMessageHandler udapClientMessageHandler) + /// + public TieredOAuthPostConfigureOptions(UdapClientMessageHandler udapClientMessageHandler, IDataProtectionProvider dataProtection) { _udapClientMessageHandler = udapClientMessageHandler; + _dataProtection = dataProtection; } /// - /// Invoked to configure a instance. + /// Invoked to configure a instance. /// /// The name of the options instance being configured. /// The options instance to configured. @@ -36,6 +41,36 @@ public void PostConfigure(string? name, TieredOAuthAuthenticationOptions options //TODO Register _udapClientMessageHandler events for logging options.BackchannelHttpHandler = _udapClientMessageHandler; - options.SignInScheme = options.SignInScheme; + options.DataProtectionProvider ??= _dataProtection; + + if (options.StateDataFormat == null) + { + var dataProtector = options.DataProtectionProvider.CreateProtector( + typeof(TieredOAuthAuthenticationHandler).FullName!, name ?? throw new ArgumentNullException(nameof(name))); + options.StateDataFormat = new PropertiesDataFormat(dataProtector); + } + + + + // + // If I go down the path of OpendIdConnectHandler remember to visit the OpenIdConnectPostConfigureOptions source for guidance + // + // if (options.StateDataFormat == null) + // { + // var dataProtector = options.DataProtectionProvider.CreateProtector( + // typeof(OpenIdConnectHandler).FullName!, name, "v1"); + // options.StateDataFormat = new PropertiesDataFormat(dataProtector); + // } + // + // if (options.StringDataFormat == null) + // { + // var dataProtector = options.DataProtectionProvider.CreateProtector( + // typeof(OpenIdConnectHandler).FullName!, + // typeof(string).FullName!, + // name, + // "v1"); + // + // options.StringDataFormat = new SecureDataFormat(new StringSerializer(), dataProtector); + // } } } \ No newline at end of file diff --git a/Udap.Server/Udap.Server.csproj b/Udap.Server/Udap.Server.csproj index cb16ff0a..54ed11da 100644 --- a/Udap.Server/Udap.Server.csproj +++ b/Udap.Server/Udap.Server.csproj @@ -31,15 +31,7 @@ - - - - - - - - - + diff --git a/Udap.Server/Validation/Default/DefaultScopeExpander.cs b/Udap.Server/Validation/Default/DefaultScopeExpander.cs index 549bf32f..ee5b466d 100644 --- a/Udap.Server/Validation/Default/DefaultScopeExpander.cs +++ b/Udap.Server/Validation/Default/DefaultScopeExpander.cs @@ -7,9 +7,12 @@ // */ #endregion +using Duende.IdentityServer.Models; + namespace Udap.Server.Validation.Default; public class DefaultScopeExpander : IScopeExpander { + /// /// Default implementation of IScopeExpander. It does nothing. /// @@ -20,12 +23,23 @@ public IEnumerable Expand(IEnumerable scopes) return scopes; } + /// + /// If the a wildcard is present such as a * then the implementation should expand accordingly. + /// + /// + /// + /// + public IEnumerable WildCardExpand(ICollection clientScopes, ICollection apiScopes) + { + return clientScopes; + } + /// /// Shrinks scope parameters. /// /// /// - public IEnumerable Shrink(IEnumerable scopes) + public IEnumerable Aggregate(IEnumerable scopes) { return scopes; } diff --git a/Udap.Server/Validation/HL7SmartScopeExpander.cs b/Udap.Server/Validation/HL7SmartScopeExpander.cs new file mode 100644 index 00000000..0ed12ad9 --- /dev/null +++ b/Udap.Server/Validation/HL7SmartScopeExpander.cs @@ -0,0 +1,205 @@ +#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.Text.RegularExpressions; +using Duende.IdentityServer.Models; +using Udap.Common.Extensions; + +namespace Udap.Server.Validation; + +/// +/// Implements rules to expand scopes where the scope parameter part may represent an encoded set of scopes. +/// +/// From HL7 FHIR SMART v2 the parameters portion of system/Patient.crud can be expanded to discrete scopes. For example:: +/// +/// system/Patient.c +/// system/Patient.r +/// system/Patient.u +/// system/Patient.d +/// +/// +public class HL7SmartScopeExpander : IScopeExpander +{ + + /// + /// Expands scope parameters to a set of discrete scopes. + /// Implement logic to determine if a scope represents a pattern that can be expanded. + /// + /// The scope parameter value. + /// A set of discrete scopes. + public IEnumerable Expand(IEnumerable scopes) + { + var expandedScopes = new HashSet(); + + foreach (var scope in scopes.ToList()) + { + var smartV2Regex = new Regex(@"^(system|user|patient)[\/].*\.[cruds]+$"); + var smartV2Matches = smartV2Regex.Matches(scope); + + var smartV1Regex = new Regex(@"^(system|user|patient)[\/].*\.(read|write)$"); + var smartV1Matches = smartV1Regex.Matches(scope); + + if (smartV2Matches.Any()) // Expand SMART V2 Scopes + { + foreach (Match match in smartV2Matches) + { + var value = match.Value; + var parts = value.Split('.'); + + var combinations = ScopeExtensions.GenerateCombinations(parts[1]); + + foreach (var parameter in combinations) + { + expandedScopes.Add($"{parts[0]}.{parameter}"); + } + } + } + else if (smartV1Matches.Any()) // Just keep SMART V1 scopes + { + expandedScopes.Add(scope); + } + else + { + if (!scope.Contains('*')) + { + expandedScopes.Add(scope); + } + } + } + + return expandedScopes; + } + + public IEnumerable ExpandToApiScopes(string scope) + { + var expandedScopes = Expand(new List { scope.Trim() }); + + return expandedScopes.Select(s => new ApiScope(s)); + } + + /// + /// Expand * resources. + /// + /// + /// + /// + public IEnumerable WildCardExpand(ICollection clientScopes, ICollection apiScopes) + { + var expandedScopes = new HashSet(); + + foreach (var scope in clientScopes.Where(s => s.Contains('*'))) + { + var smartV2Regex = new Regex(@"^(system|user|patient)\/\*\.[cruds]+$"); + var smartV2Matches = smartV2Regex.Matches(scope); + + var smartV1Regex = new Regex(@"^(system|user|patient)\/\*\.(read|write)$"); + var smartV1Matches = smartV1Regex.Matches(scope); + + if (smartV2Matches.Any()) + { + foreach (Match match in smartV2Matches) + { + var value = match.Value; + var parts = value.Split('*'); + + var specificScopes = apiScopes.Where(s => s.StartsWith(parts[0]) && + s.EndsWith(parts[1])); + + foreach (var specificScope in specificScopes) + { + expandedScopes.Add(specificScope); + } + } + } + else if (smartV1Matches.Any()) + { + foreach (Match match in smartV1Matches) + { + var value = match.Value; + var parts = value.Split('*'); + + var specificScopes = apiScopes. + Where(s => s.StartsWith(parts[0]) && + s.EndsWith(parts[1])); + + foreach (var specificScope in specificScopes) + { + expandedScopes.Add(specificScope); + } + } + } + else + { + expandedScopes.Add(scope); + } + } + + foreach (var scope in clientScopes.Where(s => !s.Contains('*'))) + { + expandedScopes.Add(scope); + } + + return expandedScopes; + } + + /// + /// Shrinks scope parameters. + /// + /// + /// + public IEnumerable Aggregate(IEnumerable scopes) + { + var matchedScopes = new HashSet(); + var unmatchedScopes = new HashSet(); + //todo cache this + var fixedOrder = new List { "c", "r", "u", "d", "s" }; + + foreach (var scope in scopes) + { + var regex = new Regex("^(system|user|patient)[\\/].*\\.[c|r|u|d|s]+$"); + if (regex.IsMatch(scope)) + { + matchedScopes.Add(scope); + } + else + { + unmatchedScopes.Add(scope); + } + } + + var scopeGroups = matchedScopes + .Select(s => + { + var parts = s.Split('.'); + + return (parts[0], parts[1]); + }) + .GroupBy((g) => (g.Item1, g.Item2)) + ; + + var shrunkScopes = new HashSet(); + + foreach (var scopeGroup in scopeGroups + .GroupBy(sc => sc.Key.Item1) + .Select(sc => sc)) + { + string groupingPrefix = scopeGroup.First().Key.Item1; + string groupingSuffix = scopeGroup.OrderByDescending(g => g.Key.Item2.Length).First().Key.Item2; + + shrunkScopes.Add($"{groupingPrefix}.{groupingSuffix}"); + } + + foreach (var unmatchedScope in unmatchedScopes) + { + shrunkScopes.Add(unmatchedScope); + } + + return shrunkScopes.OrderBy(s => s); + } +} \ No newline at end of file diff --git a/Udap.Server/Validation/IScopeExpander.cs b/Udap.Server/Validation/IScopeExpander.cs index 371d2ec8..54bd713a 100644 --- a/Udap.Server/Validation/IScopeExpander.cs +++ b/Udap.Server/Validation/IScopeExpander.cs @@ -27,10 +27,20 @@ public interface IScopeExpander /// A set of discrete scopes. IEnumerable Expand(IEnumerable scopes); + + /// + /// If the a wildcard is present such as a * then the implementation should expand accordingly. + /// + /// Client requested scopes + /// Scopes supported by server + /// + IEnumerable WildCardExpand(ICollection clientScopes, ICollection apiScopes); + + /// /// Shrinks scope parameters. /// /// /// - IEnumerable Shrink(IEnumerable scopes); + IEnumerable Aggregate(IEnumerable scopes); } diff --git a/Udap.Server/Validation/SmartV2Expander.cs b/Udap.Server/Validation/SmartV2Expander.cs deleted file mode 100644 index b99c3504..00000000 --- a/Udap.Server/Validation/SmartV2Expander.cs +++ /dev/null @@ -1,138 +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.Text.RegularExpressions; -using Duende.IdentityServer.Models; -using Udap.Common.Extensions; - -namespace Udap.Server.Validation; - -/// -/// Implements rules to expand scopes where the scope parameter part may represent an encoded set of scopes. -/// -/// From HL7 FHIR SMART v2 the parameters portion of system/Patient.crud can be expanded to discrete scopes. For example:: -/// -/// system/Patient.c -/// system/Patient.r -/// system/Patient.u -/// system/Patient.d -/// -/// -public class SmartV2Expander : IScopeExpander -{ - - /// - /// Expands scope parameters to a set of discrete scopes. - /// Implement logic to determine if a scope represents a pattern that can be expanded. - /// - /// The scope parameter value. - /// A set of discrete scopes. - public IEnumerable Expand(IEnumerable scopes) - { - var expandedScopes = new HashSet(); - - foreach (var scope in scopes.ToList()) - { - var regex = new Regex("^(system|user|patient)[\\/].*\\.[cruds]+$"); - var matches = regex.Matches(scope); - - if (matches.Any()) - { - foreach (Match match in matches) - { - var value = match.Value; - var parts = value.Split('.'); - - var combinations = ScopeExtensions.GenerateCombinations(parts[1]); - - foreach (var parameter in combinations) - { - expandedScopes.Add($"{parts[0]}.{parameter}"); - } - } - } - else - { - expandedScopes.Add(scope); - } - } - - return expandedScopes; - } - - public IEnumerable ExpandToApiScopes(string scope) - { - var expandedScopes = Expand(new List { scope }); - - return expandedScopes.Select(s => new ApiScope(s)); - } - - - /// - /// Shrinks scope parameters. - /// - /// - /// - public IEnumerable Shrink(IEnumerable scopes) - { - var matchedScopes = new HashSet(); - var unmatchedScopes = new HashSet(); - //todo cache this - var fixedOrder = new List { "c", "r", "u", "d", "s" }; - - foreach (var scope in scopes) - { - var regex = new Regex("^(system|user|patient)[\\/].*\\.[c|r|u|d|s]+$"); - if (regex.IsMatch(scope)) - { - matchedScopes.Add(scope); - } - else - { - unmatchedScopes.Add(scope); - } - } - - var scopeGroups = matchedScopes - .Select(s => - { - var parts = s.Split('.'); - - return (parts[0], parts[1]); - }) - .GroupBy((g) => (g.Item1, g.Item2)) - ; - - var shrunkScopes = new HashSet(); - - foreach (var scopeGroup in scopeGroups - .GroupBy(sc => sc.Key.Item1) - .Select(sc => sc)) - { - string groupingPrefix = scopeGroup.First().Key.Item1; - string groupingSuffix = ""; - - foreach (var item in scopeGroup - .OrderBy(sg => - { - var index = fixedOrder.IndexOf(sg.Key.Item2); - return index == -1 ? int.MaxValue : index; - })) - { - groupingSuffix += item.Key.Item2; - } - - shrunkScopes.Add($"{groupingPrefix}.{groupingSuffix}"); - } - - shrunkScopes.ToList().AddRange(unmatchedScopes.ToList()); - - return shrunkScopes; - } -} \ No newline at end of file diff --git a/_tests/Directory.Packages.props b/_tests/Directory.Packages.props index dc983d1d..05ace284 100644 --- a/_tests/Directory.Packages.props +++ b/_tests/Directory.Packages.props @@ -12,23 +12,23 @@ - - - - - - - + + + + + + + - + - - + + - + \ No newline at end of file diff --git a/_tests/Udap.PKI.Generator/BuildTestCerts.cs b/_tests/Udap.PKI.Generator/BuildTestCerts.cs index 8b9ecdc3..a8e7f80f 100644 --- a/_tests/Udap.PKI.Generator/BuildTestCerts.cs +++ b/_tests/Udap.PKI.Generator/BuildTestCerts.cs @@ -1175,6 +1175,23 @@ public void MakeCaWithIntermediateUdapForLocalhostCommunity( $"http://host.docker.internal:5033/certs/{intermediateName}.cer" ); } + + if (issuedName == "fhirLabsApiClientLocalhostCert2") + { + BuildClientCertificate( + intermediateCert, + caCert, + intermediate, + "CN=idpserver2", //issuedDistinguishedName + new List + { + "https://idpserver2", + }, + $"{LocalhostUdapIssued}/idpserver2", + $"{LocalhostCdp}/{intermediateName}.crl", + $"http://host.docker.internal:5033/certs/{intermediateName}.cer" + ); + } } @@ -1271,13 +1288,14 @@ public void MakeCaWithIntermediateUdapForLocalhostCommunity( $"{BaseDir}/../../examples/Udap.Identity.Provider/CertStore/issued/{issuedName}.pfx", true); - // Udap.Server.Tests :: Identity Provider + // Udap.Server.Tests :: Identity Provider 1 if (issuedName == "fhirLabsApiClientLocalhostCert") { File.Copy($"{LocalhostUdapIssued}/idpserver.pfx", $"{BaseDir}/../../_tests/UdapServer.Tests/CertStore/issued/idpserver.pfx", true); + File.Copy($"{communityStorePath}/{anchorName}.cer", $"{BaseDir}/../../_tests/UdapServer.Tests/CertStore/anchors/{anchorName}.cer", true); @@ -1286,6 +1304,20 @@ public void MakeCaWithIntermediateUdapForLocalhostCommunity( true); } + // Udap.Server.Tests :: Identity Provider 2 + if (issuedName == "fhirLabsApiClientLocalhostCert2") + { + File.Copy($"{LocalhostUdapIssued}/idpserver2.pfx", + $"{BaseDir}/../../_tests/UdapServer.Tests/CertStore/issued/idpserver2.pfx", + true); + + File.Copy($"{communityStorePath}/{anchorName}.cer", + $"{BaseDir}/../../_tests/UdapServer.Tests/CertStore/anchors/{anchorName}.cer", + true); + File.Copy($"{LocalhostUdapIntermediates}/{intermediateName}.cer", + $"{BaseDir}/../../_tests/UdapServer.Tests/CertStore/intermediates/{intermediateName}.cer", + true); + } // Udap.Identity.Provider.2 :: Second Identity Provider if (issuedName == "fhirLabsApiClientLocalhostCert2") diff --git a/_tests/UdapServer.Tests/Common/TestExtensions.cs b/_tests/UdapServer.Tests/Common/TestExtensions.cs index 13a058d5..7ea592db 100644 --- a/_tests/UdapServer.Tests/Common/TestExtensions.cs +++ b/_tests/UdapServer.Tests/Common/TestExtensions.cs @@ -1,13 +1,21 @@ -using Microsoft.AspNetCore.Authentication; -using Microsoft.Extensions.Options; +#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.Diagnostics; +using Microsoft.AspNetCore.Authentication; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; -using Udap.Client.Client; -using Udap.Server.Security.Authentication.TieredOAuth; -using Udap.Common.Certificates; using Microsoft.Extensions.Logging; -using FluentAssertions.Common; +using Microsoft.Extensions.Options; +using Udap.Client.Client; using Udap.Client.Configuration; +using Udap.Server.Security.Authentication.TieredOAuth; namespace UdapServer.Tests.Common; public static class TestExtensions @@ -18,19 +26,47 @@ public static class TestExtensions /// /// The authentication builder. /// The delegate used to configure the Tiered OAuth options. - /// Inject delegating handler attached to HttpClient + /// Wire httpClient to WebHostBuilder test harness + /// Wire httpClient to WebHostBuilder test harness /// The . public static AuthenticationBuilder AddTieredOAuthForTests( this AuthenticationBuilder builder, Action configuration, - UdapIdentityServerPipeline pipeline) + UdapAuthServerPipeline authServerPipeline, + UdapIdentityServerPipeline pipelineIdp1, + UdapIdentityServerPipeline pipelineIdp2) { builder.Services.AddScoped(sp => - new UdapClient( - pipeline.BrowserClient, + { + var dynamicIdp = sp.GetRequiredService(); + + if (dynamicIdp.Name == "https://idpserver") + { + Debug.Assert(pipelineIdp1.BackChannelClient != null, "pipelineIdp1.BackChannelClient != null"); + return new UdapClient( + pipelineIdp1.BackChannelClient, + sp.GetRequiredService(), + sp.GetRequiredService>(), + sp.GetRequiredService>()); + } + + if (dynamicIdp.Name == "https://idpserver2") + { + Debug.Assert(pipelineIdp2.BackChannelClient != null, "pipelineIdp2.BackChannelClient != null"); + return new UdapClient( + pipelineIdp2.BackChannelClient, + sp.GetRequiredService(), + sp.GetRequiredService>(), + sp.GetRequiredService>()); + } + + return new UdapClient( + authServerPipeline.BrowserClient, sp.GetRequiredService(), sp.GetRequiredService>(), - sp.GetRequiredService>())); + sp.GetRequiredService>()); + + }); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); diff --git a/_tests/UdapServer.Tests/Common/UdapAuthServerPipeline.cs b/_tests/UdapServer.Tests/Common/UdapAuthServerPipeline.cs index f28452ca..958f9b7a 100644 --- a/_tests/UdapServer.Tests/Common/UdapAuthServerPipeline.cs +++ b/_tests/UdapServer.Tests/Common/UdapAuthServerPipeline.cs @@ -17,7 +17,6 @@ using Duende.IdentityServer.Configuration; using Duende.IdentityServer.Events; using Duende.IdentityServer.Models; -using Duende.IdentityServer.ResponseHandling; using Duende.IdentityServer.Services; using Duende.IdentityServer.Test; using FluentAssertions; @@ -35,15 +34,14 @@ using Microsoft.Extensions.Options; using Moq; using Udap.Auth.Server.Pages; +using Udap.Client.Client; using Udap.Common; using Udap.Common.Certificates; using Udap.Common.Models; using Udap.Server.Configuration.DependencyInjection; using Udap.Server.Registration; -using Udap.Server.ResponseHandling; using Udap.Server.Security.Authentication.TieredOAuth; using UnitTests.Common; -using AuthorizeResponse = IdentityModel.Client.AuthorizeResponse; using Constants = Udap.Server.Constants; namespace UdapServer.Tests.Common; @@ -89,19 +87,19 @@ public class UdapAuthServerPipeline public BrowserClient BrowserClient { get; set; } public HttpClient BackChannelClient { get; set; } - public MockMessageHandler BackChannelMessageHandler { get; set; } = new MockMessageHandler(); - public MockMessageHandler JwtRequestMessageHandler { get; set; } = new MockMessageHandler(); + public MockMessageHandler BackChannelMessageHandler { get; set; } = new(); + public MockMessageHandler JwtRequestMessageHandler { get; set; } = new(); public TestEventService EventService = new TestEventService(); - public event Action OnPreConfigureServices = (ctx, services) => { }; - public event Action OnPostConfigureServices = services => { }; - public event Action OnPreConfigure = app => { }; - public event Action OnPostConfigure = app => { }; + public event Action OnPreConfigureServices = (_, _) => { }; + public event Action OnPostConfigureServices = _ => { }; + public event Action OnPreConfigure = _ => { }; + public event Action OnPostConfigure = _ => { }; - public Func> OnFederatedSignout; + public Func>? OnFederatedSignOut; - public void Initialize(string basePath = null, bool enableLogging = false) + public void Initialize(string? basePath = null, bool enableLogging = false) { var builder = new WebHostBuilder(); builder.ConfigureServices(ConfigureServices); @@ -120,11 +118,11 @@ public void Initialize(string basePath = null, bool enableLogging = false) } }); - builder.ConfigureAppConfiguration(configure => configure.AddJsonFile("appsettings.json")); + builder.ConfigureAppConfiguration(configure => configure.AddJsonFile("appsettings.Auth.json")); if (enableLogging) { - builder.ConfigureLogging((ctx, b) => + builder.ConfigureLogging((_, b) => { b.AddConsole(c => c.LogToStandardErrorThreshold = LogLevel.Debug); b.SetMinimumLevel(LogLevel.Trace); @@ -146,6 +144,8 @@ public void ConfigureServices(WebHostBuilderContext builder, IServiceCollection OnPreConfigureServices(builder, services); + services.AddSingleton(); + // services.AddAuthentication(opts => // { // opts.AddScheme("external", scheme => @@ -157,21 +157,22 @@ public void ConfigureServices(WebHostBuilderContext builder, IServiceCollection services.AddTransient(svcs => { var handler = new MockExternalAuthenticationHandler(svcs.GetRequiredService()); - if (OnFederatedSignout != null) handler.OnFederatedSignout = OnFederatedSignout; + if (OnFederatedSignOut != null) handler.OnFederatedSignout = OnFederatedSignOut; return handler; }); services.AddSingleton(sp => new TrustAnchorFileStore( sp.GetRequiredService>(), - new Mock>().Object)); - + new Mock>().Object)); + services.AddUdapServer(BaseUrl, "FhirLabsApi") .AddUdapInMemoryApiScopes(ApiScopes) .AddInMemoryUdapCertificates(Communities) - .AddUdapResponseGenerators() - .AddSmartV2Expander(); + .AddUdapResponseGenerators(); + // .AddTieredOAuthDynamicProvider(); + //.AddSmartV2Expander(); services.AddIdentityServer(options => @@ -268,13 +269,25 @@ public void ConfigureApp(IApplicationBuilder app) path.Run(async ctx => await OnExternalLoginCallback(ctx, new Mock().Object)); }); + app.Map("/udap.logo.48x48.png", path => + { + path.Run(ctx => OnLogo(ctx)); + }); + OnPostConfigure(app); } - + + private Task OnLogo(HttpContext context) + { + context.Response.ContentType = "image/png"; + context.Response.StatusCode = (int)HttpStatusCode.OK; + return Task.CompletedTask; + } + public bool LoginWasCalled { get; set; } public string LoginReturnUrl { get; set; } public AuthorizationRequest LoginRequest { get; set; } - public ClaimsPrincipal Subject { get; set; } + public ClaimsPrincipal? Subject { get; set; } private async Task OnRegister(HttpContext ctx) { @@ -292,28 +305,20 @@ private async Task OnExternalLoginChallenge(HttpContext ctx) //TODO: factor this code into library code and share with the Challenge.cshtml.cs file var interactionService = ctx.RequestServices.GetRequiredService(); var returnUrl = ctx.Request.Query["returnUrl"].FirstOrDefault(); - - if (interactionService.IsValidReturnUrl(returnUrl) == false) - { - throw new Exception("invalid return URL"); - } + var scheme = ctx.Request.Query["scheme"]; + var udapClient = ctx.RequestServices.GetService(); - var scheme = ctx.Request.Query["scheme"].FirstOrDefault(); - ; - var props = new AuthenticationProperties - { - RedirectUri = "/externallogin/callback", - - Items = - { - { "returnUrl", returnUrl }, - { "scheme", scheme }, - } - }; + var props = await TieredOAuthHelpers.BuildDynamicTieredOAuthOptions( + interactionService, + udapClient, + scheme.ToString(), + "/externallogin/callback", + returnUrl); // When calling ChallengeAsync your handler will be called if it is registered. - await ctx.ChallengeAsync(TieredOAuthAuthenticationDefaults.AuthenticationScheme, props); + await ctx.ChallengeAsync(scheme, props); } + private async Task OnExternalLoginCallback(HttpContext ctx, ILogger logger) { @@ -324,7 +329,7 @@ private async Task OnExternalLoginCallback(HttpContext ctx, ILogger logger) // read external identity from the temporary cookie var result = await ctx.AuthenticateAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme); - if (result?.Succeeded != true) + if (result.Succeeded != true) { throw new Exception("External authentication error"); } @@ -647,4 +652,9 @@ public T Resolve() queryParams.TryGetValue("state", out var state); return state.SingleOrDefault(); } +} + +public class DynamicIdp +{ + public string? Name { get; set; } } \ No newline at end of file diff --git a/_tests/UdapServer.Tests/Common/UdapIdentityServerPipeline.cs b/_tests/UdapServer.Tests/Common/UdapIdentityServerPipeline.cs index ffbd7328..ba3ef017 100644 --- a/_tests/UdapServer.Tests/Common/UdapIdentityServerPipeline.cs +++ b/_tests/UdapServer.Tests/Common/UdapIdentityServerPipeline.cs @@ -12,24 +12,19 @@ // See LICENSE in the project root for license information. - using System.IdentityModel.Tokens.Jwt; using System.Net; using System.Security.Claims; using System.Security.Cryptography.X509Certificates; -using System.Security.Policy; using System.Text.Json; using Duende.IdentityServer; using Duende.IdentityServer.Configuration; -using Duende.IdentityServer.Extensions; using Duende.IdentityServer.Models; using Duende.IdentityServer.Services; using Duende.IdentityServer.Test; using FluentAssertions; -using Google.Api; using IdentityModel.Client; using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Authentication.OAuth; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; @@ -39,63 +34,93 @@ using Microsoft.Extensions.Logging; using Udap.Common.Certificates; using Udap.Common.Models; -using Udap.Model; using Udap.Server; using Udap.Server.Configuration.DependencyInjection; using Udap.Server.Registration; using Udap.Server.Security.Authentication.TieredOAuth; -using Udap.Server.Stores.InMemory; namespace UdapServer.Tests.Common; public class UdapIdentityServerPipeline { - public const string BaseUrl = "https://idpserver"; - public const string LoginPage = BaseUrl + "/account/login"; - public const string LogoutPage = BaseUrl + "/account/logout"; - public const string ConsentPage = BaseUrl + "/account/consent"; - public const string ErrorPage = BaseUrl + "/home/error"; - - public const string DeviceAuthorization = BaseUrl + "/connect/deviceauthorization"; - public const string DiscoveryEndpoint = BaseUrl + "/.well-known/openid-configuration"; - public const string DiscoveryKeysEndpoint = BaseUrl + "/.well-known/openid-configuration/jwks"; - public const string AuthorizeEndpoint = BaseUrl + "/connect/authorize"; - public const string BackchannelAuthenticationEndpoint = BaseUrl + "/connect/ciba"; - public const string TokenEndpoint = BaseUrl + "/connect/token"; - public const string RevocationEndpoint = BaseUrl + "/connect/revocation"; - public const string UserInfoEndpoint = BaseUrl + "/connect/userinfo"; - public const string IntrospectionEndpoint = BaseUrl + "/connect/introspect"; - public const string IdentityTokenValidationEndpoint = BaseUrl + "/connect/identityTokenValidation"; - public const string EndSessionEndpoint = BaseUrl + "/connect/endsession"; - public const string EndSessionCallbackEndpoint = BaseUrl + "/connect/endsession/callback"; - public const string CheckSessionEndpoint = BaseUrl + "/connect/checksession"; - public const string RegistrationEndpoint = BaseUrl + "/connect/register"; - - public const string FederatedSignOutPath = "/signout-oidc"; - public const string FederatedSignOutUrl = BaseUrl + FederatedSignOutPath; - - public IdentityServerOptions Options { get; set; } + public string BaseUrl => _baseUrl; + private readonly string _baseUrl = "https://idpserver"; + private readonly string? _appSettingsFile; + + public readonly string LoginPage; + public readonly string LogoutPage; + public readonly string ConsentPage; + public readonly string ErrorPage; + public readonly string DeviceAuthorization; + public readonly string DiscoveryEndpoint; + public readonly string DiscoveryKeysEndpoint; + public readonly string AuthorizeEndpoint; + public readonly string BackchannelAuthenticationEndpoint; + public readonly string TokenEndpoint; + public readonly string RevocationEndpoint; + public readonly string UserInfoEndpoint; + public readonly string IntrospectionEndpoint; + public readonly string IdentityTokenValidationEndpoint; + public readonly string EndSessionEndpoint; + public readonly string EndSessionCallbackEndpoint; + public readonly string CheckSessionEndpoint; + public readonly string RegistrationEndpoint; + public readonly string FederatedSignOutPath; + public readonly string FederatedSignOutUrl; + + public UdapIdentityServerPipeline(string? baseUrl = null, string? appSettingsFile = null) + { + _baseUrl = baseUrl ?? _baseUrl; + _appSettingsFile = appSettingsFile; + + LoginPage = BaseUrl + "/account/login"; + LogoutPage = BaseUrl + "/account/logout"; + ConsentPage = BaseUrl + "/account/consent"; + ErrorPage = BaseUrl + "/home/error"; + + DeviceAuthorization = BaseUrl + "/connect/deviceauthorization"; + DiscoveryEndpoint = BaseUrl + "/.well-known/openid-configuration"; + DiscoveryKeysEndpoint = BaseUrl + "/.well-known/openid-configuration/jwks"; + AuthorizeEndpoint = BaseUrl + "/connect/authorize"; + BackchannelAuthenticationEndpoint = BaseUrl + "/connect/ciba"; + TokenEndpoint = BaseUrl + "/connect/token"; + RevocationEndpoint = BaseUrl + "/connect/revocation"; + UserInfoEndpoint = BaseUrl + "/connect/userinfo"; + IntrospectionEndpoint = BaseUrl + "/connect/introspect"; + IdentityTokenValidationEndpoint = BaseUrl + "/connect/identityTokenValidation"; + EndSessionEndpoint = BaseUrl + "/connect/endsession"; + EndSessionCallbackEndpoint = BaseUrl + "/connect/endsession/callback"; + CheckSessionEndpoint = BaseUrl + "/connect/checksession"; + RegistrationEndpoint = BaseUrl + "/connect/register"; + FederatedSignOutPath = "/signout-oidc"; + FederatedSignOutUrl = BaseUrl + FederatedSignOutPath; + } + + + + public IdentityServerOptions? Options { get; set; } public List Clients { get; set; } = new List(); public List IdentityScopes { get; set; } = new List(); public List ApiResources { get; set; } = new List(); public List ApiScopes { get; set; } = new List(); public List Users { get; set; } = new List(); public List Communities { get; set; } = new List(); - public TestServer Server { get; set; } - public HttpMessageHandler Handler { get; set; } + public TestServer? Server { get; set; } + public HttpMessageHandler? Handler { get; set; } - public BrowserClient BrowserClient { get; set; } - public HttpClient BackChannelClient { get; set; } + public BrowserClient? BrowserClient { get; set; } + public HttpClient? BackChannelClient { get; set; } public MockMessageHandler BackChannelMessageHandler { get; set; } = new MockMessageHandler(); public MockMessageHandler JwtRequestMessageHandler { get; set; } = new MockMessageHandler(); - public event Action OnPreConfigureServices = (ctx, services) => { }; - public event Action OnPostConfigureServices = services => { }; - public event Action OnPreConfigure = app => { }; - public event Action OnPostConfigure = app => { }; + public event Action OnPreConfigureServices = (_, _) => { }; + public event Action OnPostConfigureServices = _ => { }; + public event Action OnPreConfigure = _ => { }; + public event Action OnPostConfigure = _ => { }; - public Func> OnFederatedSignout; + public Func>? OnFederatedSignout; + public void Initialize(string basePath = null, bool enableLogging = false) { @@ -116,7 +141,7 @@ public void Initialize(string basePath = null, bool enableLogging = false) } }); - builder.ConfigureAppConfiguration(configure => configure.AddJsonFile("appsettings.json")); + builder.ConfigureAppConfiguration(configure => configure.AddJsonFile(_appSettingsFile ?? "appsettings.Idp1.json")); if (enableLogging) { @@ -298,9 +323,9 @@ public void ConfigureApp(IApplicationBuilder app) } public bool LoginWasCalled { get; set; } - public string LoginReturnUrl { get; set; } - public AuthorizationRequest LoginRequest { get; set; } - public ClaimsPrincipal Subject { get; set; } + public string? LoginReturnUrl { get; set; } + public AuthorizationRequest? LoginRequest { get; set; } + public ClaimsPrincipal? Subject { get; set; } private async Task OnRegister(HttpContext ctx) { @@ -371,7 +396,7 @@ private async Task IssueLoginCookie(HttpContext ctx) } public bool LogoutWasCalled { get; set; } - public LogoutRequest LogoutRequest { get; set; } + public LogoutRequest? LogoutRequest { get; set; } private async Task OnLogout(HttpContext ctx) { @@ -387,8 +412,8 @@ private async Task ReadLogoutRequest(HttpContext ctx) } public bool ConsentWasCalled { get; set; } - public AuthorizationRequest ConsentRequest { get; set; } - public ConsentResponse ConsentResponse { get; set; } + public AuthorizationRequest? ConsentRequest { get; set; } + public ConsentResponse? ConsentResponse { get; set; } private async Task OnConsent(HttpContext ctx) { @@ -418,7 +443,7 @@ private async Task CreateConsentResponse(HttpContext ctx) } public bool CustomWasCalled { get; set; } - public AuthorizationRequest CustomRequest { get; set; } + public AuthorizationRequest? CustomRequest { get; set; } private async Task OnCustom(HttpContext ctx) { @@ -428,13 +453,13 @@ private async Task OnCustom(HttpContext ctx) } public bool ErrorWasCalled { get; set; } - public ErrorMessage ErrorMessage { get; set; } - public IServiceProvider ApplicationServices { get; private set; } + public ErrorMessage? ErrorMessage { get; set; } + public IServiceProvider? ApplicationServices { get; private set; } /// /// Record the backchannel Identity Token during Tiered OAuth /// - public JwtSecurityToken IdToken { get; set; } + public JwtSecurityToken? IdToken { get; set; } private async Task OnError(HttpContext ctx) { diff --git a/_tests/UdapServer.Tests/Conformance/Basic/ClientCredentialsUdapModeTests.cs b/_tests/UdapServer.Tests/Conformance/Basic/ClientCredentialsUdapModeTests.cs index 5f4e9a96..d98b72cd 100644 --- a/_tests/UdapServer.Tests/Conformance/Basic/ClientCredentialsUdapModeTests.cs +++ b/_tests/UdapServer.Tests/Conformance/Basic/ClientCredentialsUdapModeTests.cs @@ -18,7 +18,11 @@ using IdentityModel; using IdentityModel.Client; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; +using Moq; +using Udap.Client.Client; using Udap.Client.Client.Extensions; using Udap.Client.Configuration; using Udap.Common.Models; @@ -50,20 +54,33 @@ public ClientCredentialsUdapModeTests(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 + services.AddSingleton(new ServerSettings { ServerSupport = ServerSupport.UDAP, DefaultUserScopes = "udap", DefaultSystemScopes = "udap" }); - s.AddSingleton(new UdapClientOptions - { - ClientName = "Mock Client", - Contacts = new HashSet { "mailto:Joseph.Shook@Surescripts.com", "mailto:JoeShook@gmail.com" } - }); + services.AddSingleton>(new OptionsMonitorForTests( + new UdapClientOptions + { + ClientName = "Mock Client", + Contacts = new HashSet { "mailto:Joseph.Shook@Surescripts.com", "mailto:JoeShook@gmail.com" } + + }) + ); + + // + // Ensure IUdapClient uses the TestServer's HttpMessageHandler + // Wired up via _mockPipeline.BrowserClient + // + services.AddScoped(sp => new UdapClient( + _mockPipeline.BrowserClient, + sp.GetRequiredService(), + sp.GetRequiredService>(), + sp.GetRequiredService>())); }; @@ -145,8 +162,8 @@ public ClientCredentialsUdapModeTests(ITestOutputHelper testOutputHelper) _mockPipeline.IdentityScopes.Add(new IdentityResources.OpenId()); _mockPipeline.IdentityScopes.Add(new IdentityResources.Profile()); - _mockPipeline.ApiScopes.AddRange(new SmartV2Expander().ExpandToApiScopes("system/Patient.rs")); - _mockPipeline.ApiScopes.AddRange(new SmartV2Expander().ExpandToApiScopes(" system/Appointment.rs")); + _mockPipeline.ApiScopes.AddRange(new HL7SmartScopeExpander().ExpandToApiScopes("system/Patient.rs")); + _mockPipeline.ApiScopes.AddRange(new HL7SmartScopeExpander().ExpandToApiScopes(" system/Appointment.rs")); } [Fact] @@ -163,48 +180,26 @@ public async Task GetAccessToken() { var clientCert = new X509Certificate2("CertStore/issued/fhirlabs.net.client.pfx", "udap-test"); - var document = UdapDcrBuilderForClientCredentials - .Create(clientCert) - .WithAudience(UdapAuthServerPipeline.RegistrationEndpoint) - .WithExpiration(TimeSpan.FromMinutes(5)) - .WithJwtId() - .WithClientName("mock test") - .WithContacts(new HashSet - { - "mailto:Joseph.Shook@Surescripts.com", "mailto:JoeShook@gmail.com" - }) - .WithTokenEndpointAuthMethod(UdapConstants.RegistrationDocumentValues.TokenEndpointAuthMethodValue) - .WithScope("system/Patient.rs") - .Build(); - - var signedSoftwareStatement = - SignedSoftwareStatementBuilder - .Create(clientCert, document) - .Build(); - - var requestBody = new UdapRegisterRequest - ( - signedSoftwareStatement, - UdapConstants.UdapVersionsSupportedValue, - new string[] { } - ); - - var regResponse = await _mockPipeline.BrowserClient.PostAsync( - UdapAuthServerPipeline.RegistrationEndpoint, - new StringContent(JsonSerializer.Serialize(requestBody), new MediaTypeHeaderValue("application/json"))); - - regResponse.StatusCode.Should().Be(HttpStatusCode.Created); - var regDocumentResult = await regResponse.Content.ReadFromJsonAsync(); + var udapClient = _mockPipeline.Resolve(); + // + // Typically the client would validate a server before proceeding to registration. + // + udapClient.UdapServerMetaData = new UdapMetadata(new Mock().Object, new Mock>().Object) + { 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, + regDocumentResult.ClientId, IdentityServerPipeline.TokenEndpoint, new List() { @@ -248,40 +243,19 @@ public async Task GetAccessTokenECDSA() var clientCert = new X509Certificate2("CertStore/issued/fhirlabs.net.ecdsa.client.pfx", "udap-test", X509KeyStorageFlags.Exportable); - var document = UdapDcrBuilderForClientCredentials - .Create(clientCert) - .WithAudience(UdapAuthServerPipeline.RegistrationEndpoint) - .WithExpiration(TimeSpan.FromMinutes(5)) - .WithJwtId() - .WithClientName("mock test") - .WithContacts(new HashSet - { - "mailto:Joseph.Shook@Surescripts.com", "mailto:JoeShook@gmail.com" - }) - .WithTokenEndpointAuthMethod(UdapConstants.RegistrationDocumentValues.TokenEndpointAuthMethodValue) - .WithScope("system/Patient.rs") - .Build(); - - var signedSoftwareStatement = - SignedSoftwareStatementBuilder - .Create(clientCert, document) - .BuildECDSA(); - - var requestBody = new UdapRegisterRequest - ( - signedSoftwareStatement, - UdapConstants.UdapVersionsSupportedValue, - new string[] { } - ); - - var regResponse = await _mockPipeline.BrowserClient.PostAsync( - UdapAuthServerPipeline.RegistrationEndpoint, - new StringContent(JsonSerializer.Serialize(requestBody), new MediaTypeHeaderValue("application/json"))); + var udapClient = _mockPipeline.Resolve(); - regResponse.StatusCode.Should().Be(HttpStatusCode.Created); - var regDocumentResult = await regResponse.Content.ReadFromJsonAsync(); + // + // Typically the client would validate a server before proceeding to registration. + // + udapClient.UdapServerMetaData = new UdapMetadata(new Mock().Object, new Mock>().Object) + { RegistrationEndpoint = UdapAuthServerPipeline.RegistrationEndpoint }; + var regDocumentResult = await udapClient.RegisterClientCredentialsClient( + clientCert, + "system/Patient.rs"); + regDocumentResult.GetError().Should().BeNull(); // @@ -335,38 +309,19 @@ public async Task UpdateRegistration() // // First Registration // - var document = UdapDcrBuilderForClientCredentials - .Create(clientCert) - .WithAudience(UdapAuthServerPipeline.RegistrationEndpoint) - .WithExpiration(TimeSpan.FromMinutes(5)) - .WithJwtId() - .WithClientName("mock test") - .WithContacts(new HashSet - { - "mailto:Joseph.Shook@Surescripts.com", "mailto:JoeShook@gmail.com" - }) - .WithTokenEndpointAuthMethod(UdapConstants.RegistrationDocumentValues.TokenEndpointAuthMethodValue) - .WithScope("system/Patient.rs") - .Build(); + var udapClient = _mockPipeline.Resolve(); - var signedSoftwareStatement = - SignedSoftwareStatementBuilder - .Create(clientCert, document) - .Build(); - - var requestBody = new UdapRegisterRequest - ( - signedSoftwareStatement, - UdapConstants.UdapVersionsSupportedValue, - new string[] { } - ); + // + // Typically the client would validate a server before proceeding to registration. + // + udapClient.UdapServerMetaData = new UdapMetadata(new Mock().Object, new Mock>().Object) + { RegistrationEndpoint = UdapAuthServerPipeline.RegistrationEndpoint }; - var regResponse = await _mockPipeline.BrowserClient.PostAsync( - UdapAuthServerPipeline.RegistrationEndpoint, - new StringContent(JsonSerializer.Serialize(requestBody), new MediaTypeHeaderValue("application/json"))); + var regDocumentResult = await udapClient.RegisterClientCredentialsClient( + clientCert, + "system/Patient.rs"); - regResponse.StatusCode.Should().Be(HttpStatusCode.Created); - var regDocumentResult = await regResponse.Content.ReadFromJsonAsync(); + regDocumentResult.GetError().Should().BeNull(); regDocumentResult!.Scope.Should().Be("system/Patient.rs"); var clientIdWithDefaultSubAltName = regDocumentResult.ClientId; @@ -374,82 +329,25 @@ public async Task UpdateRegistration() // // Second Registration // - document = UdapDcrBuilderForClientCredentials - .Create(clientCert) - .WithAudience(UdapAuthServerPipeline.RegistrationEndpoint) - .WithExpiration(TimeSpan.FromMinutes(5)) - .WithJwtId() - .WithClientName("mock test") - .WithContacts(new HashSet - { - "mailto:Joseph.Shook@Surescripts.com", "mailto:JoeShook@gmail.com" - }) - .WithTokenEndpointAuthMethod(UdapConstants.RegistrationDocumentValues.TokenEndpointAuthMethodValue) - .WithScope("system/Patient.rs system/Appointment.rs") - .Build(); + regDocumentResult = await udapClient.RegisterClientCredentialsClient( + clientCert, + "system/Patient.rs system/Appointment.rs"); - signedSoftwareStatement = - SignedSoftwareStatementBuilder - .Create(clientCert, document) - .Build(); + regDocumentResult.GetError().Should().BeNull(); + regDocumentResult!.Scope.Should().Be("system/Appointment.rs system/Patient.rs"); - requestBody = new UdapRegisterRequest - ( - signedSoftwareStatement, - UdapConstants.UdapVersionsSupportedValue, - new string[] { } - ); - - regResponse = await _mockPipeline.BrowserClient.PostAsync( - UdapAuthServerPipeline.RegistrationEndpoint, - new StringContent(JsonSerializer.Serialize(requestBody), new MediaTypeHeaderValue("application/json"))); - - regResponse.StatusCode.Should().Be(HttpStatusCode.OK); - regDocumentResult = await regResponse.Content.ReadFromJsonAsync(); - regDocumentResult!.Scope.Should().Be("system/Patient.rs system/Appointment.rs"); - - regDocumentResult!.ClientId.Should().Be(clientIdWithDefaultSubAltName); + regDocumentResult.ClientId.Should().Be(clientIdWithDefaultSubAltName); // // Third Registration with different Uri Subject Alt Name from same client certificate // expect 201 created because I changed the SAN selected by calling WithIssuer // - - document = UdapDcrBuilderForClientCredentials - .Create(clientCert) - .WithIssuer(new Uri("https://fhirlabs.net:7016/fhir/r4")) - .WithAudience(UdapAuthServerPipeline.RegistrationEndpoint) - .WithExpiration(TimeSpan.FromMinutes(5)) - .WithJwtId() - .WithClientName("mock test") - .WithContacts(new HashSet - { - "mailto:Joseph.Shook@Surescripts.com", "mailto:JoeShook@gmail.com" - }) - .WithTokenEndpointAuthMethod(UdapConstants.RegistrationDocumentValues.TokenEndpointAuthMethodValue) - .WithScope("system/Patient.rs system/Appointment.rs") - .Build(); - - signedSoftwareStatement = - SignedSoftwareStatementBuilder - .Create(clientCert, document) - .Build(); - - requestBody = new UdapRegisterRequest - ( - signedSoftwareStatement, - UdapConstants.UdapVersionsSupportedValue, - new string[] { } - ); - - regResponse = await _mockPipeline.BrowserClient.PostAsync( - UdapAuthServerPipeline.RegistrationEndpoint, - new StringContent(JsonSerializer.Serialize(requestBody), new MediaTypeHeaderValue("application/json"))); - - regResponse.StatusCode.Should().Be(HttpStatusCode.Created, await regResponse.Content.ReadAsStringAsync()); - var regDocumentResultForSelectedSubAltName = - await regResponse.Content.ReadFromJsonAsync(); - regDocumentResultForSelectedSubAltName!.Scope.Should().Be("system/Patient.rs system/Appointment.rs"); + var regDocumentResultForSelectedSubAltName = await udapClient.RegisterClientCredentialsClient( + clientCert, + "system/Patient.rs system/Appointment.rs", + "https://fhirlabs.net:7016/fhir/r4"); + + regDocumentResultForSelectedSubAltName!.Scope.Should().Be("system/Appointment.rs system/Patient.rs"); var clientIdWithSelectedSubAltName = regDocumentResultForSelectedSubAltName.ClientId; clientIdWithSelectedSubAltName.Should().NotBe(clientIdWithDefaultSubAltName); @@ -457,48 +355,16 @@ public async Task UpdateRegistration() // Fourth Registration with Uri Subject Alt Name from third registration // expect 200 OK because I changed scope // - document = UdapDcrBuilderForClientCredentials - .Create(clientCert) - .WithIssuer(new Uri("https://fhirlabs.net:7016/fhir/r4")) - .WithAudience(UdapAuthServerPipeline.RegistrationEndpoint) - .WithExpiration(TimeSpan.FromMinutes(5)) - .WithJwtId() - .WithClientName("mock test") - .WithContacts(new HashSet - { - "mailto:Joseph.Shook@Surescripts.com", "mailto:JoeShook@gmail.com" - }) - .WithTokenEndpointAuthMethod(UdapConstants.RegistrationDocumentValues.TokenEndpointAuthMethodValue) - .WithScope("system/Patient.rs") - .Build(); - - signedSoftwareStatement = - SignedSoftwareStatementBuilder - .Create(clientCert, document) - .Build(); - - requestBody = new UdapRegisterRequest - ( - signedSoftwareStatement, - UdapConstants.UdapVersionsSupportedValue, - new string[] { } - ); - - regResponse = await _mockPipeline.BrowserClient.PostAsync( - UdapAuthServerPipeline.RegistrationEndpoint, - new StringContent(JsonSerializer.Serialize(requestBody), new MediaTypeHeaderValue("application/json"))); - - regResponse.StatusCode.Should().Be(HttpStatusCode.OK); - var regDocumentResultForSelectedSubAltNameSecond = - await regResponse.Content.ReadFromJsonAsync(); + var regDocumentResultForSelectedSubAltNameSecond = await udapClient.RegisterClientCredentialsClient( + clientCert, + "system/Patient.rs", + "https://fhirlabs.net:7016/fhir/r4"); + regDocumentResultForSelectedSubAltNameSecond!.Scope.Should().Be("system/Patient.rs"); - - regDocumentResultForSelectedSubAltNameSecond!.ClientId.Should().Be(clientIdWithSelectedSubAltName); + regDocumentResultForSelectedSubAltNameSecond.ClientId.Should().Be(clientIdWithSelectedSubAltName); } - - [Fact] public async Task CancelRegistration() @@ -636,7 +502,7 @@ public async Task CancelRegistration() regResponse.StatusCode.Should().Be(HttpStatusCode.Created, await regResponse.Content.ReadAsStringAsync()); var regDocumentResultForSelectedSubAltName = await regResponse.Content.ReadFromJsonAsync(); - regDocumentResultForSelectedSubAltName!.Scope.Should().Be("system/Patient.rs system/Appointment.rs"); + regDocumentResultForSelectedSubAltName!.Scope.Should().Be("system/Appointment.rs system/Patient.rs"); var clientIdWithSelectedSubAltName = regDocumentResultForSelectedSubAltName.ClientId; clientIdWithSelectedSubAltName.Should().NotBe(clientIdWithDefaultSubAltName); @@ -696,14 +562,14 @@ public async Task CancelRegistration() [Fact] public async Task RegisterTwoCommunitiesWithSameISS_AndCancelOne() { - var clientCert_1 = new X509Certificate2("CertStore/issued/fhirlabs.net.client.pfx", "udap-test"); - var clientCert_2 = new X509Certificate2("CertStore/issued/fhirLabsApiClientLocalhostCert2.pfx", "udap-test"); + var clientCert1 = new X509Certificate2("CertStore/issued/fhirlabs.net.client.pfx", "udap-test"); + var clientCert2 = new X509Certificate2("CertStore/issued/fhirLabsApiClientLocalhostCert2.pfx", "udap-test"); // // Register Client 1 from community "udap://fhirlabs.net" // var document = UdapDcrBuilderForClientCredentials - .Create(clientCert_1) + .Create(clientCert1) .WithAudience(UdapAuthServerPipeline.RegistrationEndpoint) .WithExpiration(TimeSpan.FromMinutes(5)) .WithJwtId() @@ -718,7 +584,7 @@ public async Task RegisterTwoCommunitiesWithSameISS_AndCancelOne() var signedSoftwareStatement = SignedSoftwareStatementBuilder - .Create(clientCert_1, document) + .Create(clientCert1, document) .Build(); var requestBody = new UdapRegisterRequest @@ -741,7 +607,7 @@ public async Task RegisterTwoCommunitiesWithSameISS_AndCancelOne() // Register Client 2 from community "localhost_fhirlabs_community2" // document = UdapDcrBuilderForClientCredentials - .Create(clientCert_2) + .Create(clientCert2) .WithAudience(UdapAuthServerPipeline.RegistrationEndpoint) .WithExpiration(TimeSpan.FromMinutes(5)) .WithJwtId() @@ -756,7 +622,7 @@ public async Task RegisterTwoCommunitiesWithSameISS_AndCancelOne() signedSoftwareStatement = SignedSoftwareStatementBuilder - .Create(clientCert_2, document) + .Create(clientCert2, document) .Build(); requestBody = new UdapRegisterRequest @@ -780,7 +646,7 @@ public async Task RegisterTwoCommunitiesWithSameISS_AndCancelOne() // Cancel Registration from community "udap://fhirlabs.net" // document = UdapDcrBuilderForClientCredentials - .Cancel(clientCert_1) + .Cancel(clientCert1) .WithAudience(UdapAuthServerPipeline.RegistrationEndpoint) .WithExpiration(TimeSpan.FromMinutes(5)) .WithJwtId() @@ -795,7 +661,7 @@ public async Task RegisterTwoCommunitiesWithSameISS_AndCancelOne() signedSoftwareStatement = SignedSoftwareStatementBuilder - .Create(clientCert_1, document) + .Create(clientCert1, document) .Build(); requestBody = new UdapRegisterRequest @@ -956,7 +822,7 @@ public async Task Missing_grant_types_RegistrationResultsIn_invalid_client() regResponse.StatusCode.Should().Be(HttpStatusCode.Created, await regResponse.Content.ReadAsStringAsync()); var regDocumentResultForSelectedSubAltName = await regResponse.Content.ReadFromJsonAsync(); - regDocumentResultForSelectedSubAltName!.Scope.Should().Be("system/Patient.rs system/Appointment.rs"); + regDocumentResultForSelectedSubAltName!.Scope.Should().Be("system/Appointment.rs system/Patient.rs"); var clientIdWithSelectedSubAltName = regDocumentResultForSelectedSubAltName.ClientId; clientIdWithSelectedSubAltName.Should().NotBe(clientIdWithDefaultSubAltName); diff --git a/_tests/UdapServer.Tests/Conformance/Basic/RegistrationAndChangeRegistrationTests.cs b/_tests/UdapServer.Tests/Conformance/Basic/RegistrationAndChangeRegistrationTests.cs index ed1bf577..4aa423c8 100644 --- a/_tests/UdapServer.Tests/Conformance/Basic/RegistrationAndChangeRegistrationTests.cs +++ b/_tests/UdapServer.Tests/Conformance/Basic/RegistrationAndChangeRegistrationTests.cs @@ -142,7 +142,7 @@ public RegistrationAndChangeRegistrationTests(ITestOutputHelper testOutputHelper _mockPipeline.IdentityScopes.Add(new IdentityResources.OpenId()); _mockPipeline.IdentityScopes.Add(new IdentityResources.Profile()); _mockPipeline.ApiScopes.Add(new ApiScope("system/Patient.rs")); - _mockPipeline.ApiScopes.Add(new ApiScope(" system/Appointment.rs")); + _mockPipeline.ApiScopes.Add(new ApiScope("system/Appointment.rs")); } @@ -203,7 +203,7 @@ public async Task RegisterClientCredentialsThenRegisterAuthorizationCode() .WithExpiration(TimeSpan.FromMinutes(5)) .WithJwtId() .WithClientName("mock test") - .WithLogoUri("https://example.com/logo.png") + .WithLogoUri("https://avatars.githubusercontent.com/u/77421324?s=48&v=4") .WithContacts(new HashSet { "mailto:Joseph.Shook@Surescripts.com", "mailto:JoeShook@gmail.com" @@ -234,7 +234,7 @@ public async Task RegisterClientCredentialsThenRegisterAuthorizationCode() regResponse.StatusCode.Should().Be(HttpStatusCode.OK, await regResponse.Content.ReadAsStringAsync()); regDocumentResult = await regResponse.Content.ReadFromJsonAsync(); - regDocumentResult!.Scope.Should().Be("system/Patient.rs system/Appointment.rs"); + regDocumentResult!.Scope.Should().Be("system/Appointment.rs system/Patient.rs"); regDocumentResult!.ClientId.Should().Be(clientId); _mockPipeline.Clients.Single().AllowedGrantTypes.Should().NotContain(OidcConstants.GrantTypes.ClientCredentials); diff --git a/_tests/UdapServer.Tests/Conformance/Basic/ScopeExpansionTests.cs b/_tests/UdapServer.Tests/Conformance/Basic/ScopeExpansionTests.cs index 90bf7ce8..dc874e6b 100644 --- a/_tests/UdapServer.Tests/Conformance/Basic/ScopeExpansionTests.cs +++ b/_tests/UdapServer.Tests/Conformance/Basic/ScopeExpansionTests.cs @@ -14,28 +14,27 @@ using System.Security.Cryptography.X509Certificates; using System.Text.Json; using Duende.IdentityServer.Models; +using Duende.IdentityServer.Test; using FluentAssertions; -using IdentityModel.Client; using IdentityModel; +using IdentityModel.Client; +using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.DependencyInjection; using Microsoft.IdentityModel.Tokens; using Udap.Client.Client.Extensions; +using Udap.Client.Configuration; +using Udap.Common.Extensions; using Udap.Common.Models; using Udap.Model; using Udap.Model.Access; using Udap.Model.Registration; using Udap.Model.Statement; using Udap.Server.Configuration; +using Udap.Server.Models; +using Udap.Server.Validation; using Udap.Util.Extensions; using UdapServer.Tests.Common; using Xunit.Abstractions; -using Microsoft.AspNetCore.WebUtilities; -using Udap.Server.Models; -using Duende.IdentityServer.Test; -using System.Text; -using Udap.Client.Configuration; -using Udap.Common.Extensions; -using Udap.Server.Validation; namespace UdapServer.Tests.Conformance.Basic; @@ -44,7 +43,7 @@ namespace UdapServer.Tests.Conformance.Basic; public class ScopeExpansionTests { private readonly ITestOutputHelper _testOutputHelper; - private UdapAuthServerPipeline _mockPipeline = new UdapAuthServerPipeline(); + private readonly UdapAuthServerPipeline _mockPipeline = new(); public ScopeExpansionTests(ITestOutputHelper testOutputHelper) { @@ -55,19 +54,20 @@ public ScopeExpansionTests(ITestOutputHelper testOutputHelper) _mockPipeline.OnPostConfigureServices += s => { - s.AddSingleton(new ServerSettings + s.AddSingleton(new ServerSettings { ServerSupport = ServerSupport.UDAP, DefaultUserScopes = "udap", DefaultSystemScopes = "udap" }); - s.AddSingleton(new UdapClientOptions + s.AddSingleton(new UdapClientOptions { ClientName = "Mock Client", Contacts = new HashSet { "mailto:Joseph.Shook@Surescripts.com", "mailto:JoeShook@gmail.com" } }); - + + s.AddScoped(); }; _mockPipeline.OnPreConfigureServices += (_, s) => @@ -99,7 +99,7 @@ public ScopeExpansionTests(ITestOutputHelper testOutputHelper) Enabled = true, Intermediates = new List() { - new Intermediate + new() { BeginDate = intermediateCert.NotBefore.ToUniversalTime(), EndDate = intermediateCert.NotAfter.ToUniversalTime(), @@ -112,9 +112,13 @@ public ScopeExpansionTests(ITestOutputHelper testOutputHelper) } } }); - _mockPipeline.ApiScopes.AddRange(new SmartV2Expander().ExpandToApiScopes("system/Patient.cruds")); - _mockPipeline.ApiScopes.AddRange(new SmartV2Expander().ExpandToApiScopes("system/Encounter.r")); - _mockPipeline.ApiScopes.AddRange(new SmartV2Expander().ExpandToApiScopes("system/Condition.s")); + + _mockPipeline.ApiScopes.AddRange(new HL7SmartScopeExpander().ExpandToApiScopes("system/Patient.cruds")); + _mockPipeline.ApiScopes.AddRange(new HL7SmartScopeExpander().ExpandToApiScopes("system/Encounter.r")); + _mockPipeline.ApiScopes.AddRange(new HL7SmartScopeExpander().ExpandToApiScopes("system/Condition.s")); + _mockPipeline.ApiScopes.Add( new ApiScope("system/Practitioner.read")); + + _mockPipeline.IdentityScopes.Add(new IdentityResources.OpenId()); _mockPipeline.IdentityScopes.Add(new UdapIdentityResources.Udap()); @@ -122,7 +126,7 @@ public ScopeExpansionTests(ITestOutputHelper testOutputHelper) { SubjectId = "bob", Username = "bob", - Claims = new Claim[] + Claims = new[] { new Claim("name", "Bob Loblaw"), new Claim("email", "bob@loblaw.com"), @@ -144,7 +148,7 @@ public void GenerateCombinations_ReturnsUniqueStringCombinationsInGivenOrder(str } } - + [Fact] public async Task ScopeV2WithClientCredentialsTest() { @@ -158,13 +162,13 @@ public async Task ScopeV2WithClientCredentialsTest() // var now = DateTime.UtcNow; var jwtPayload = new JwtPayLoadExtension( - resultDocument!.ClientId, + resultDocument.ClientId, IdentityServerPipeline.TokenEndpoint, new List() { - new Claim(JwtClaimTypes.Subject, resultDocument.ClientId!), - new Claim(JwtClaimTypes.IssuedAt, EpochTime.GetIntDate(now.ToUniversalTime()).ToString(), ClaimValueTypes.Integer), - new Claim(JwtClaimTypes.JwtId, CryptoRandom.CreateUniqueId()), + new(JwtClaimTypes.Subject, resultDocument.ClientId!), + new(JwtClaimTypes.IssuedAt, EpochTime.GetIntDate(now.ToUniversalTime()).ToString(), ClaimValueTypes.Integer), + new(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(), @@ -199,13 +203,13 @@ public async Task ScopeV2WithClientCredentialsTest() // jwtPayload = new JwtPayLoadExtension( - resultDocument!.ClientId, + resultDocument.ClientId, IdentityServerPipeline.TokenEndpoint, new List() { - new Claim(JwtClaimTypes.Subject, resultDocument.ClientId!), - new Claim(JwtClaimTypes.IssuedAt, EpochTime.GetIntDate(now.ToUniversalTime()).ToString(), ClaimValueTypes.Integer), - new Claim(JwtClaimTypes.JwtId, CryptoRandom.CreateUniqueId()), + new(JwtClaimTypes.Subject, resultDocument.ClientId!), + new(JwtClaimTypes.IssuedAt, EpochTime.GetIntDate(now.ToUniversalTime()).ToString(), ClaimValueTypes.Integer), + new(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(), @@ -241,13 +245,13 @@ public async Task ScopeV2WithClientCredentialsTest() // jwtPayload = new JwtPayLoadExtension( - resultDocument!.ClientId, + resultDocument.ClientId, IdentityServerPipeline.TokenEndpoint, new List() { - new Claim(JwtClaimTypes.Subject, resultDocument.ClientId!), - new Claim(JwtClaimTypes.IssuedAt, EpochTime.GetIntDate(now.ToUniversalTime()).ToString(), ClaimValueTypes.Integer), - new Claim(JwtClaimTypes.JwtId, CryptoRandom.CreateUniqueId()), + new(JwtClaimTypes.Subject, resultDocument.ClientId!), + new(JwtClaimTypes.IssuedAt, EpochTime.GetIntDate(now.ToUniversalTime()).ToString(), ClaimValueTypes.Integer), + new(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(), @@ -277,59 +281,19 @@ public async Task ScopeV2WithClientCredentialsTest() tokenResponse.Scope.Should().Be("system/Patient.rs", tokenResponse.Raw); - // - // Again wild card expansion: TODO - // - - // jwtPayload = new JwtPayLoadExtension( - // resultDocument!.ClientId, - // IdentityServerPipeline.TokenEndpoint, - // new List() - // { - // new Claim(JwtClaimTypes.Subject, resultDocument.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() - // ); - // - // clientAssertion = - // SignedSoftwareStatementBuilder - // .Create(clientCert, jwtPayload) - // .Build("RS384"); - // - // - // 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.*" - // }; - // - // tokenResponse = await _mockPipeline.BackChannelClient.UdapRequestClientCredentialsTokenAsync(clientRequest); - // - // tokenResponse.Scope.Should().Be("system/Patient.rs", tokenResponse.Raw); - + // // Again negative // jwtPayload = new JwtPayLoadExtension( - resultDocument!.ClientId, + resultDocument.ClientId, IdentityServerPipeline.TokenEndpoint, new List() { - new Claim(JwtClaimTypes.Subject, resultDocument.ClientId!), - new Claim(JwtClaimTypes.IssuedAt, EpochTime.GetIntDate(now.ToUniversalTime()).ToString(), ClaimValueTypes.Integer), - new Claim(JwtClaimTypes.JwtId, CryptoRandom.CreateUniqueId()), + new(JwtClaimTypes.Subject, resultDocument.ClientId!), + new(JwtClaimTypes.IssuedAt, EpochTime.GetIntDate(now.ToUniversalTime()).ToString(), ClaimValueTypes.Integer), + new(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(), @@ -361,6 +325,150 @@ public async Task ScopeV2WithClientCredentialsTest() tokenResponse.Error.Should().Be("invalid_scope"); } + [Fact] + public async Task ScopeV2WithClientCredentialsExtendedTest() + { + var clientCert = new X509Certificate2("CertStore/issued/fhirlabs.net.client.pfx", "udap-test"); + var resultDocument = await RegisterClientWithAuthServer("system/Patient.rs", clientCert); + resultDocument.Should().NotBeNull(); + resultDocument!.ClientId.Should().NotBeNull(); + + resultDocument.Scope.Should().Be("system/Patient.rs"); + _mockPipeline.Clients[0].AllowedScopes.Count.Should().Be(3); + + var now = DateTime.UtcNow; + var jwtPayload = new JwtPayLoadExtension( + resultDocument.ClientId, + IdentityServerPipeline.TokenEndpoint, + new List() + { + new(JwtClaimTypes.Subject, resultDocument.ClientId!), + new(JwtClaimTypes.IssuedAt, EpochTime.GetIntDate(now.ToUniversalTime()).ToString(), ClaimValueTypes.Integer), + new(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 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 system/Patient.r system/Patient.s" + }; + + var tokenResponse = await _mockPipeline.BackChannelClient.UdapRequestClientCredentialsTokenAsync(clientRequest); + tokenResponse.Scope.Should().Be("system/Patient.r system/Patient.rs system/Patient.s", tokenResponse.Raw); + } + + [Fact] + public async Task ScopeV2WithClientCredentialsExtended2Test() + { + var clientCert = new X509Certificate2("CertStore/issued/fhirlabs.net.client.pfx", "udap-test"); + var resultDocument = await RegisterClientWithAuthServer("system/*.rs", clientCert); + resultDocument.Should().NotBeNull(); + resultDocument!.ClientId.Should().NotBeNull(); + + resultDocument.Scope.Should().Be("system/Condition.s system/Encounter.r system/Patient.rs"); + + var now = DateTime.UtcNow; + var jwtPayload = new JwtPayLoadExtension( + resultDocument.ClientId, + IdentityServerPipeline.TokenEndpoint, + new List() + { + new(JwtClaimTypes.Subject, resultDocument.ClientId!), + new(JwtClaimTypes.IssuedAt, EpochTime.GetIntDate(now.ToUniversalTime()).ToString(), ClaimValueTypes.Integer), + new(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 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/Condition.s system/Encounter.r system/Patient.rs" + }; + + var tokenResponse = await _mockPipeline.BackChannelClient.UdapRequestClientCredentialsTokenAsync(clientRequest); + tokenResponse.Scope.Should().Be("system/Condition.s system/Encounter.r system/Patient.rs", tokenResponse.Raw); + } + + [Fact] + public async Task ScopeV2WithClientCredentialsWildcardTest() + { + var clientCert = new X509Certificate2("CertStore/issued/fhirlabs.net.client.pfx", "udap-test"); + var resultDocument = await RegisterClientWithAuthServer("system/*.read", clientCert); + resultDocument.Should().NotBeNull(); + resultDocument!.ClientId.Should().NotBeNull(); + + resultDocument.Scope.Should().Be("system/Practitioner.read"); + + var now = DateTime.UtcNow; + var jwtPayload = new JwtPayLoadExtension( + resultDocument.ClientId, + IdentityServerPipeline.TokenEndpoint, + new List() + { + new(JwtClaimTypes.Subject, resultDocument.ClientId!), + new(JwtClaimTypes.IssuedAt, EpochTime.GetIntDate(now.ToUniversalTime()).ToString(), ClaimValueTypes.Integer), + new(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 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/Practitioner.read" + }; + + var tokenResponse = await _mockPipeline.BackChannelClient.UdapRequestClientCredentialsTokenAsync(clientRequest); + tokenResponse.Scope.Should().Be("system/Practitioner.read", tokenResponse.Raw); + } [Fact] public async Task ScopeV2WithAuthCodeTest() { @@ -372,7 +480,7 @@ public async Task ScopeV2WithAuthCodeTest() .WithExpiration(TimeSpan.FromMinutes(5)) .WithJwtId() .WithClientName("mock test") - .WithLogoUri("https://example.com/logo.png") + .WithLogoUri("https://avatars.githubusercontent.com/u/77421324?s=48&v=4") .WithContacts(new HashSet { "mailto:Joseph.Shook@Surescripts.com", "mailto:JoeShook@gmail.com" @@ -414,7 +522,7 @@ public async Task ScopeV2WithAuthCodeTest() await _mockPipeline.LoginAsync("bob"); var url = _mockPipeline.CreateAuthorizeUrl( - clientId: resultDocument!.ClientId!, + clientId: resultDocument.ClientId!, responseType: "code", scope: "openid system/Patient.rs", redirectUri: "https://code_client/callback", @@ -427,8 +535,7 @@ public async Task ScopeV2WithAuthCodeTest() response.StatusCode.Should().Be(HttpStatusCode.Redirect, await response.Content.ReadAsStringAsync()); response.Headers.Location.Should().NotBeNull(); - var redirectUri = response.Headers.Location!.AbsoluteUri; - response.Headers.Location!.AbsoluteUri.Should().Contain("https://code_client/callback"); + response.Headers.Location!.AbsoluteUri.Should().Contain("https://code_client/callback"); // _testOutputHelper.WriteLine(response.Headers.Location!.AbsoluteUri); var queryParams = QueryHelpers.ParseQuery(response.Headers.Location.Query); queryParams.Should().Contain(p => p.Key == "code"); diff --git a/_tests/UdapServer.Tests/Conformance/Basic/UdapForceStateParamFalseTests.cs b/_tests/UdapServer.Tests/Conformance/Basic/UdapForceStateParamFalseTests.cs index df01a602..2656682a 100644 --- a/_tests/UdapServer.Tests/Conformance/Basic/UdapForceStateParamFalseTests.cs +++ b/_tests/UdapServer.Tests/Conformance/Basic/UdapForceStateParamFalseTests.cs @@ -146,7 +146,7 @@ public async Task Request_state_missing_results_in_success() .WithExpiration(TimeSpan.FromMinutes(5)) .WithJwtId() .WithClientName("mock test") - .WithLogoUri("https://example.com/logo.png") + .WithLogoUri("https://avatars.githubusercontent.com/u/77421324?s=48&v=4") .WithContacts(new HashSet { "mailto:Joseph.Shook@Surescripts.com", "mailto:JoeShook@gmail.com" diff --git a/_tests/UdapServer.Tests/Conformance/Basic/UdapResponseTypeResponseModeTests.cs b/_tests/UdapServer.Tests/Conformance/Basic/UdapResponseTypeResponseModeTests.cs index a063fa49..a4eaf80d 100644 --- a/_tests/UdapServer.Tests/Conformance/Basic/UdapResponseTypeResponseModeTests.cs +++ b/_tests/UdapServer.Tests/Conformance/Basic/UdapResponseTypeResponseModeTests.cs @@ -144,7 +144,7 @@ public async Task Request_response_type_missing_results_in_unsupported_response_ .WithExpiration(TimeSpan.FromMinutes(5)) .WithJwtId() .WithClientName("mock test") - .WithLogoUri("https://example.com/logo.png") + .WithLogoUri("https://avatars.githubusercontent.com/u/77421324?s=48&v=4") .WithContacts(new HashSet { "mailto:Joseph.Shook@Surescripts.com", "mailto:JoeShook@gmail.com" @@ -223,7 +223,7 @@ public async Task Request_state_missing_results_in_unsupported_response_type() .WithExpiration(TimeSpan.FromMinutes(5)) .WithJwtId() .WithClientName("mock test") - .WithLogoUri("https://example.com/logo.png") + .WithLogoUri("https://avatars.githubusercontent.com/u/77421324?s=48&v=4") .WithContacts(new HashSet { "mailto:Joseph.Shook@Surescripts.com", "mailto:JoeShook@gmail.com" @@ -296,7 +296,7 @@ public async Task Request_response_type_invalid_results_in_unsupported_response_ .WithExpiration(TimeSpan.FromMinutes(5)) .WithJwtId() .WithClientName("mock test") - .WithLogoUri("https://example.com/logo.png") + .WithLogoUri("https://avatars.githubusercontent.com/u/77421324?s=48&v=4") .WithContacts(new HashSet { "mailto:Joseph.Shook@Surescripts.com", "mailto:JoeShook@gmail.com" @@ -370,7 +370,7 @@ public async Task Request_client_id_missing_results_in_invalid_request() .WithExpiration(TimeSpan.FromMinutes(5)) .WithJwtId() .WithClientName("mock test") - .WithLogoUri("https://example.com/logo.png") + .WithLogoUri("https://avatars.githubusercontent.com/u/77421324?s=48&v=4") .WithContacts(new HashSet { "mailto:Joseph.Shook@Surescripts.com", "mailto:JoeShook@gmail.com" @@ -439,7 +439,7 @@ public async Task Request_client_id_invalid_results_in_unauthorized_client() .WithExpiration(TimeSpan.FromMinutes(5)) .WithJwtId() .WithClientName("mock test") - .WithLogoUri("https://example.com/logo.png") + .WithLogoUri("https://avatars.githubusercontent.com/u/77421324?s=48&v=4") .WithContacts(new HashSet { "mailto:Joseph.Shook@Surescripts.com", "mailto:JoeShook@gmail.com" @@ -505,7 +505,7 @@ public async Task Request_accepted() .WithExpiration(TimeSpan.FromMinutes(5)) .WithJwtId() .WithClientName("mock test") - .WithLogoUri("https://example.com/logo.png") + .WithLogoUri("https://avatars.githubusercontent.com/u/77421324?s=48&v=4") .WithContacts(new HashSet { "mailto:Joseph.Shook@Surescripts.com", "mailto:JoeShook@gmail.com" @@ -583,7 +583,7 @@ public async Task Request_accepted_RegisterWithDifferentRedirectUrl() .WithExpiration(TimeSpan.FromMinutes(5)) .WithJwtId() .WithClientName("mock test") - .WithLogoUri("https://example.com/logo.png") + .WithLogoUri("https://avatars.githubusercontent.com/u/77421324?s=48&v=4") .WithContacts(new HashSet { "mailto:Joseph.Shook@Surescripts.com", "mailto:JoeShook@gmail.com" @@ -724,7 +724,7 @@ public async Task Request_accepted_URI_HostOnly() .WithExpiration(TimeSpan.FromMinutes(5)) .WithJwtId() .WithClientName("mock test") - .WithLogoUri("https://example.com/logo.png") + .WithLogoUri("https://avatars.githubusercontent.com/u/77421324?s=48&v=4") .WithContacts(new HashSet { "mailto:Joseph.Shook@Surescripts.com", "mailto:JoeShook@gmail.com" diff --git a/_tests/UdapServer.Tests/Conformance/Tiered/TieredOauthTests.cs b/_tests/UdapServer.Tests/Conformance/Tiered/TieredOauthTests.cs index 0877b919..7619b18e 100644 --- a/_tests/UdapServer.Tests/Conformance/Tiered/TieredOauthTests.cs +++ b/_tests/UdapServer.Tests/Conformance/Tiered/TieredOauthTests.cs @@ -7,10 +7,9 @@ // */ #endregion +using System.Diagnostics; using System.IdentityModel.Tokens.Jwt; using System.Net; -using System.Net.Http.Headers; -using System.Net.Http.Json; using System.Security.Claims; using System.Security.Cryptography.X509Certificates; using System.Text; @@ -20,12 +19,13 @@ using Duende.IdentityServer.Test; using FluentAssertions; using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; +using Moq; using Udap.Client.Client; using Udap.Client.Configuration; using Udap.Common; @@ -34,7 +34,6 @@ using Udap.Model; using Udap.Model.Access; using Udap.Model.Registration; -using Udap.Model.Statement; using Udap.Server.Configuration; using Udap.Server.Models; using Udap.Server.Security.Authentication.TieredOAuth; @@ -49,51 +48,54 @@ public class TieredOauthTests { private readonly ITestOutputHelper _testOutputHelper; - private UdapAuthServerPipeline _mockAuthorServerPipeline = new UdapAuthServerPipeline(); - private UdapIdentityServerPipeline _mockIdPPipeline = new UdapIdentityServerPipeline(); + private readonly UdapAuthServerPipeline _mockAuthorServerPipeline = new(); + private readonly UdapIdentityServerPipeline _mockIdPPipeline = new(); + private readonly UdapIdentityServerPipeline _mockIdPPipeline2 = new("https://idpserver2", "appsettings.Idp2.json"); - private IAuthenticationSchemeProvider _schemeProvider; + private readonly X509Certificate2 _community1Anchor; + private readonly X509Certificate2 _community1IntermediateCert; + private readonly X509Certificate2 _community2Anchor; + private readonly X509Certificate2 _community2IntermediateCert; public TieredOauthTests(ITestOutputHelper testOutputHelper) { _testOutputHelper = testOutputHelper; - var sureFhirLabsAnchor = new X509Certificate2("CertStore/anchors/caLocalhostCert.cer"); - var intermediateCert = new X509Certificate2("CertStore/intermediates/intermediateLocalhostCert.cer"); + _community1Anchor = new X509Certificate2("CertStore/anchors/caLocalhostCert.cer"); + _community1IntermediateCert = new X509Certificate2("CertStore/intermediates/intermediateLocalhostCert.cer"); - var idpAnchor1 = new X509Certificate2("CertStore/anchors/caLocalhostCert.cer"); - var idpIntermediate1 = new X509Certificate2("CertStore/intermediates/intermediateLocalhostCert.cer"); - - BuildUdapAuthorizationServer(sureFhirLabsAnchor, intermediateCert); - BuildUdapIdentityProvider(idpAnchor1, idpIntermediate1); + _community2Anchor = new X509Certificate2("CertStore/anchors/caLocalhostCert2.cer"); + _community2IntermediateCert = new X509Certificate2("CertStore/intermediates/intermediateLocalhostCert2.cer"); + + BuildUdapAuthorizationServer(); + BuildUdapIdentityProvider1(); + BuildUdapIdentityProvider2(); } - private void BuildUdapAuthorizationServer(X509Certificate2 sureFhirLabsAnchor, X509Certificate2 intermediateCert) + private void BuildUdapAuthorizationServer() { - _mockAuthorServerPipeline.OnPostConfigureServices += s => + _mockAuthorServerPipeline.OnPostConfigureServices += services => { - s.AddSingleton(new ServerSettings + services.AddSingleton(new ServerSettings { ServerSupport = ServerSupport.Hl7SecurityIG, - IdPMappings = new List - { - new IdPMapping() - { - Scheme = "TieredOAuth", // default name - IdpBaseUrl = "https://idpserver" - } - } // DefaultUserScopes = "udap", // DefaultSystemScopes = "udap" // ForceStateParamOnAuthorizationCode = false (default) }); - s.AddSingleton>(new OptionsMonitorForTests( + services.AddSingleton>(new OptionsMonitorForTests( new UdapClientOptions { ClientName = "AuthServer Client", - Contacts = new HashSet { "mailto:Joseph.Shook@Surescripts.com", "mailto:JoeShook@gmail.com" } + Contacts = new HashSet { "mailto:Joseph.Shook@Surescripts.com", "mailto:JoeShook@gmail.com" }, + TieredOAuthClientLogo = "https://server/udap.logo.48x48.png" }) ); + + // + // Allow logo resolve back to udap.auth server + // + services.AddSingleton(_ => _mockAuthorServerPipeline.BrowserClient); }; _mockAuthorServerPipeline.OnPreConfigureServices += (builderContext, services) => @@ -103,28 +105,33 @@ private void BuildUdapAuthorizationServer(X509Certificate2 sureFhirLabsAnchor, X // It was not intended to work with the concept of a dynamic client registration. services.AddSingleton(_mockAuthorServerPipeline.Clients); - services.Configure(builderContext.Configuration.GetSection(Udap.Common.Constants.UDAP_FILE_STORE_MANIFEST)); + services.Configure(builderContext.Configuration.GetSection(Constants.UDAP_FILE_STORE_MANIFEST)); services.AddAuthentication() - // - // By convention the scheme name should match the community name in UdapFileCertStoreManifest - // to allow discovery of the IdPBaseUrl - // - .AddTieredOAuthForTests(options => - { - options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme; - options.AuthorizationEndpoint = "https://idpserver/connect/authorize"; - options.TokenEndpoint = "https://idpserver/connect/token"; - options.IdPBaseUrl = "https://idpserver"; - }, _mockIdPPipeline); // point backchannel to the IdP + // + // By convention the scheme name should match the community name in UdapFileCertStoreManifest + // to allow discovery of the IdPBaseUrl + // + .AddTieredOAuthForTests(options => + { + options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme; + }, + _mockAuthorServerPipeline, + _mockIdPPipeline, + _mockIdPPipeline2); + - services.AddAuthorization(); // required for TieredOAuth Testing + + services.ConfigureAll(options => + { + options.BackchannelHttpHandler = _mockIdPPipeline2.Server?.CreateHandler(); + }); + + using var serviceProvider = services.BuildServiceProvider(); - _schemeProvider = serviceProvider.GetRequiredService(); - }; _mockAuthorServerPipeline.OnPostConfigure += app => @@ -140,29 +147,30 @@ private void BuildUdapAuthorizationServer(X509Certificate2 sureFhirLabsAnchor, X _mockAuthorServerPipeline.Communities.Add(new Community { - Name = "udap://fhirlabs.net", + Id = 0, + Name = "https://idpserver", Enabled = true, Default = true, Anchors = new[] { new Anchor { - BeginDate = sureFhirLabsAnchor.NotBefore.ToUniversalTime(), - EndDate = sureFhirLabsAnchor.NotAfter.ToUniversalTime(), - Name = sureFhirLabsAnchor.Subject, - Community = "udap://fhirlabs.net", - Certificate = sureFhirLabsAnchor.ToPemFormat(), - Thumbprint = sureFhirLabsAnchor.Thumbprint, + BeginDate = _community1Anchor.NotBefore.ToUniversalTime(), + EndDate = _community1Anchor.NotAfter.ToUniversalTime(), + Name = _community1Anchor.Subject, + Community = "https://idpserver", + Certificate = _community1Anchor.ToPemFormat(), + Thumbprint = _community1Anchor.Thumbprint, Enabled = true, Intermediates = new List() { - new Intermediate + new() { - BeginDate = intermediateCert.NotBefore.ToUniversalTime(), - EndDate = intermediateCert.NotAfter.ToUniversalTime(), - Name = intermediateCert.Subject, - Certificate = intermediateCert.ToPemFormat(), - Thumbprint = intermediateCert.Thumbprint, + BeginDate = _community1IntermediateCert.NotBefore.ToUniversalTime(), + EndDate = _community1IntermediateCert.NotAfter.ToUniversalTime(), + Name = _community1IntermediateCert.Subject, + Certificate = _community1IntermediateCert.ToPemFormat(), + Thumbprint = _community1IntermediateCert.Thumbprint, Enabled = true } } @@ -170,10 +178,42 @@ private void BuildUdapAuthorizationServer(X509Certificate2 sureFhirLabsAnchor, X } }); - - // _mockAuthorServerPipeline. + _mockAuthorServerPipeline.Communities.Add(new Community + { + Id = 1, + Name = "udap://idp-community-2", + Enabled = true, + Default = true, + Anchors = new[] + { + new Anchor + { + BeginDate = _community2Anchor.NotBefore.ToUniversalTime(), + EndDate = _community2Anchor.NotAfter.ToUniversalTime(), + Name = _community2Anchor.Subject, + Community = "udap://idp-community-2", + Certificate = _community2Anchor.ToPemFormat(), + Thumbprint = _community2Anchor.Thumbprint, + Enabled = true, + Intermediates = new List() + { + new() + { + BeginDate = _community2IntermediateCert.NotBefore.ToUniversalTime(), + EndDate = _community2IntermediateCert.NotAfter.ToUniversalTime(), + Name = _community2IntermediateCert.Subject, + Certificate = _community2IntermediateCert.ToPemFormat(), + Thumbprint = _community2IntermediateCert.Thumbprint, + Enabled = true + } + } + } + } + }); + // _mockAuthorServerPipeline. + _mockAuthorServerPipeline.IdentityScopes.Add(new IdentityResources.OpenId()); _mockAuthorServerPipeline.IdentityScopes.Add(new IdentityResources.Profile()); @@ -185,7 +225,7 @@ private void BuildUdapAuthorizationServer(X509Certificate2 sureFhirLabsAnchor, X { SubjectId = "bob", Username = "bob", - Claims = new Claim[] + Claims = new[] { new Claim("name", "Bob Loblaw"), new Claim("email", "bob@loblaw.com"), @@ -196,11 +236,11 @@ private void BuildUdapAuthorizationServer(X509Certificate2 sureFhirLabsAnchor, X _mockAuthorServerPipeline.UserStore = new TestUserStore(_mockAuthorServerPipeline.Users); } - private void BuildUdapIdentityProvider(X509Certificate2 sureFhirLabsAnchor, X509Certificate2 intermediateCert) + private void BuildUdapIdentityProvider1() { - _mockIdPPipeline.OnPostConfigureServices += s => + _mockIdPPipeline.OnPostConfigureServices += services => { - s.AddSingleton(new ServerSettings + services.AddSingleton(new ServerSettings { ServerSupport = ServerSupport.UDAP, DefaultUserScopes = "udap", @@ -208,44 +248,48 @@ private void BuildUdapIdentityProvider(X509Certificate2 sureFhirLabsAnchor, X509 // ForceStateParamOnAuthorizationCode = false (default) AlwaysIncludeUserClaimsInIdToken = true }); - }; - _mockIdPPipeline.OnPreConfigureServices += (builderContext, services) => - { // 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. // It was not intended to work with the concept of a dynamic client registration. services.AddSingleton(_mockIdPPipeline.Clients); + + // + // Allow logo resolve back to udap.auth server + // + services.AddSingleton(sp => _mockAuthorServerPipeline.BrowserClient); }; + _mockIdPPipeline.Initialize(enableLogging: true); + Debug.Assert(_mockIdPPipeline.BrowserClient != null, "_mockIdPPipeline.BrowserClient != null"); _mockIdPPipeline.BrowserClient.AllowAutoRedirect = false; _mockIdPPipeline.Communities.Add(new Community { - Name = "udap://fhirlabs.net", + Name = "udap://idp-community-1", Enabled = true, Default = true, Anchors = new[] { new Anchor { - BeginDate = sureFhirLabsAnchor.NotBefore.ToUniversalTime(), - EndDate = sureFhirLabsAnchor.NotAfter.ToUniversalTime(), - Name = sureFhirLabsAnchor.Subject, - Community = "udap://fhirlabs.net", - Certificate = sureFhirLabsAnchor.ToPemFormat(), - Thumbprint = sureFhirLabsAnchor.Thumbprint, + BeginDate = _community1Anchor.NotBefore.ToUniversalTime(), + EndDate = _community1Anchor.NotAfter.ToUniversalTime(), + Name = _community1Anchor.Subject, + Community = "udap://idp-community-1", + Certificate = _community1Anchor.ToPemFormat(), + Thumbprint = _community1Anchor.Thumbprint, Enabled = true, Intermediates = new List() { - new Intermediate + new() { - BeginDate = intermediateCert.NotBefore.ToUniversalTime(), - EndDate = intermediateCert.NotAfter.ToUniversalTime(), - Name = intermediateCert.Subject, - Certificate = intermediateCert.ToPemFormat(), - Thumbprint = intermediateCert.Thumbprint, + BeginDate = _community1IntermediateCert.NotBefore.ToUniversalTime(), + EndDate = _community1IntermediateCert.NotAfter.ToUniversalTime(), + Name = _community1IntermediateCert.Subject, + Certificate = _community1IntermediateCert.ToPemFormat(), + Thumbprint = _community1IntermediateCert.Thumbprint, Enabled = true } } @@ -263,7 +307,7 @@ private void BuildUdapIdentityProvider(X509Certificate2 sureFhirLabsAnchor, X509 { SubjectId = "bob", Username = "bob", - Claims = new Claim[] + Claims = new[] { new Claim("name", "Bob Loblaw"), new Claim("email", "bob@loblaw.com"), @@ -276,6 +320,91 @@ private void BuildUdapIdentityProvider(X509Certificate2 sureFhirLabsAnchor, X509 _mockIdPPipeline.Subject = new IdentityServerUser("bob").CreatePrincipal(); } + private void BuildUdapIdentityProvider2() + { + _mockIdPPipeline2.OnPostConfigureServices += services => + { + services.AddSingleton(new ServerSettings + { + ServerSupport = ServerSupport.UDAP, + DefaultUserScopes = "udap", + DefaultSystemScopes = "udap", + // ForceStateParamOnAuthorizationCode = false (default) + AlwaysIncludeUserClaimsInIdToken = true + }); + + // 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. + // It was not intended to work with the concept of a dynamic client registration. + services.AddSingleton(_mockIdPPipeline2.Clients); + + // + // Allow logo resolve back to udap.auth server + // + services.AddSingleton(sp => _mockAuthorServerPipeline.BrowserClient); + }; + + + + _mockIdPPipeline2.Initialize(enableLogging: true); + Debug.Assert(_mockIdPPipeline2.BrowserClient != null, "_mockIdPPipeline2.BrowserClient != null"); + _mockIdPPipeline2.BrowserClient.AllowAutoRedirect = false; + + _mockIdPPipeline2.Communities.Add(new Community + { + Name = "udap://idp-community-2", + Enabled = true, + Default = true, + Anchors = new[] + { + new Anchor + { + BeginDate = _community2Anchor.NotBefore.ToUniversalTime(), + EndDate = _community2Anchor.NotAfter.ToUniversalTime(), + Name = _community2Anchor.Subject, + Community = "udap://idp-community-2", + Certificate = _community2Anchor.ToPemFormat(), + Thumbprint = _community2Anchor.Thumbprint, + Enabled = true, + Intermediates = new List() + { + new() + { + BeginDate = _community2IntermediateCert.NotBefore.ToUniversalTime(), + EndDate = _community2IntermediateCert.NotAfter.ToUniversalTime(), + Name = _community2IntermediateCert.Subject, + Certificate = _community2IntermediateCert.ToPemFormat(), + Thumbprint = _community2IntermediateCert.Thumbprint, + Enabled = true + } + } + } + } + }); + + _mockIdPPipeline2.IdentityScopes.Add(new IdentityResources.OpenId()); + _mockIdPPipeline2.IdentityScopes.Add(new UdapIdentityResources.Profile()); + _mockIdPPipeline2.IdentityScopes.Add(new UdapIdentityResources.Udap()); + _mockIdPPipeline2.IdentityScopes.Add(new IdentityResources.Email()); + _mockIdPPipeline2.IdentityScopes.Add(new UdapIdentityResources.FhirUser()); + + _mockIdPPipeline2.Users.Add(new TestUser + { + SubjectId = "bob", + Username = "bob", + Claims = new[] + { + new Claim("name", "Bob Loblaw"), + new Claim("email", "bob@loblaw.com"), + new Claim("role", "Attorney"), + new Claim("hl7_identifier", "123") + } + }); + + // Allow pipeline to sign in during Login + _mockIdPPipeline2.Subject = new IdentityServerUser("bob").CreatePrincipal(); + } + /// /// /// @@ -292,11 +421,13 @@ public async Task ClientAuthorize_IdPDiscovery_IdPRegistration_IdPAuthAccess_Cli 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(); @@ -335,7 +466,7 @@ public async Task ClientAuthorize_IdPDiscovery_IdPRegistration_IdPAuthAccess_Cli queryParams.Single(q => q.Key == "state").Value.Should().BeEquivalentTo(clientState); queryParams.Single(q => q.Key == "idp").Value.Should().BeEquivalentTo("https://idpserver"); - var schemes = await _schemeProvider.GetAllSchemesAsync(); + var schemes = await _mockAuthorServerPipeline.Resolve().GetAllSchemesAsync(); var sb = new StringBuilder(); sb.Append("https://server/externallogin/challenge?"); // built in UdapAccount/Login/Index.cshtml.cs @@ -377,24 +508,25 @@ public async Task ClientAuthorize_IdPDiscovery_IdPRegistration_IdPAuthAccess_Cli // 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/signin-tieredoauth", new TieredOAuthAuthenticationOptions().CorrelationCookie.Name).Should().BeNull(); + _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/signin-tieredoauth", new TieredOAuthAuthenticationOptions().CorrelationCookie.Name).Should().NotBeNull(); + _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); - var backChannelClientId = QueryHelpers.ParseQuery(backChannelChallengeResponse.Headers.Location.Query).Single(p => p.Key == "client_id").Value.ToString(); + 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); @@ -414,9 +546,9 @@ public async Task ClientAuthorize_IdPDiscovery_IdPRegistration_IdPAuthAccess_Cli // _testOutputHelper.WriteLine(authorizeCallbackResult.Headers.Location!.OriginalString); authorizeCallbackResult.StatusCode.Should().Be(HttpStatusCode.Redirect, await authorizeCallbackResult.Content.ReadAsStringAsync()); authorizeCallbackResult.Headers.Location.Should().NotBeNull(); - authorizeCallbackResult.Headers.Location!.AbsoluteUri.Should().StartWith("https://server/signin-tieredoauth?"); + authorizeCallbackResult.Headers.Location!.AbsoluteUri.Should().StartWith("https://server/federation/udap-tiered/signin?"); - var backChannelCode = QueryHelpers.ParseQuery(authorizeCallbackResult.Headers.Location.Query).Single(p => p.Key == "code").Value.ToString(); + QueryHelpers.ParseQuery(authorizeCallbackResult.Headers.Location.Query).Single(p => p.Key == "code").Value.Should().NotBeEmpty(); // // Validate backchannel state is the same @@ -431,7 +563,7 @@ public async Task ClientAuthorize_IdPDiscovery_IdPRegistration_IdPAuthAccess_Cli _mockAuthorServerPipeline.GetSessionCookie().Should().BeNull(); _mockAuthorServerPipeline.BrowserClient.GetCookie("https://server", "idsrv").Should().BeNull(); - // Run Auth Server /signin-tieredoauth This is the Registered scheme callback endpoint + // Run Auth Server /federation/udap-tiered/signin This is the Registered scheme callback endpoint // Allow one redirect to run /connect/token. // Sets Cookies: idsrv.external idsrv.session, and idsrv // Backchannel calls: @@ -447,7 +579,7 @@ public async Task ClientAuthorize_IdPDiscovery_IdPRegistration_IdPAuthAccess_Cli _mockAuthorServerPipeline.BrowserClient.AllowCookies = true; - // "https://server/signin-tieredoauth?..." + // "https://server/federation/udap-tiered/signin?..." var schemeCallbackResult = await _mockAuthorServerPipeline.BrowserClient.GetAsync(authorizeCallbackResult.Headers.Location!.AbsoluteUri); @@ -465,8 +597,8 @@ public async Task ClientAuthorize_IdPDiscovery_IdPRegistration_IdPAuthAccess_Cli // Check the IdToken in the back channel. Ensure the HL7_Identifier is in the claims // // _testOutputHelper.WriteLine(_mockIdPPipeline.IdToken.ToString()); - - _mockIdPPipeline.IdToken.Claims.Should().Contain(c => c.Type == "hl7_identifier"); + _mockIdPPipeline.IdToken.Should().NotBeNull(); + _mockIdPPipeline.IdToken!.Claims.Should().Contain(c => c.Type == "hl7_identifier"); _mockIdPPipeline.IdToken.Claims.Single(c => c.Type == "hl7_identifier").Value.Should().Be("123"); // Run the authServer https://server/connect/authorize/callback @@ -507,19 +639,15 @@ public async Task ClientAuthorize_IdPDiscovery_IdPRegistration_IdPAuthAccess_Cli .Build(); - var udapClient = new UdapClient( - _mockAuthorServerPipeline.BrowserClient, - _mockAuthorServerPipeline.Resolve(), - _mockAuthorServerPipeline.Resolve>(), - _mockAuthorServerPipeline.Resolve>()); + dynamicIdp.Name = null; // Influence UdapClient resolution in AddTieredOAuthForTests. + var udapClient = _mockAuthorServerPipeline.Resolve(); + var accessToken = await udapClient.ExchangeCodeForTokenResponse(tokenRequest); accessToken.Should().NotBeNull(); accessToken.IdentityToken.Should().NotBeNull(); var jwt = new JwtSecurityToken(accessToken.IdentityToken); - - var at = new JwtSecurityToken(accessToken.AccessToken); - + new JwtSecurityToken(accessToken.AccessToken).Should().NotBeNull(); using var jsonDocument = JsonDocument.Parse(jwt.Payload.SerializeToJson()); var formattedStatement = JsonSerializer.Serialize( @@ -571,48 +699,311 @@ public async Task ClientAuthorize_IdPDiscovery_IdPRegistration_IdPAuthAccess_Cli */ } - private async Task RegisterClientWithAuthServer() + [Fact] //(Skip = "Dynamic Tiered OAuth Provider WIP")] + public async Task Tiered_OAuth_With_DynamicProvider() { - var clientCert = new X509Certificate2("CertStore/issued/fhirLabsApiClientLocalhostCert.pfx", "udap-test"); + // Register client with auth server + var resultDocument = await RegisterClientWithAuthServer(); + _mockAuthorServerPipeline.RemoveSessionCookie(); + _mockAuthorServerPipeline.RemoveLoginCookie(); + resultDocument.Should().NotBeNull(); + resultDocument!.ClientId.Should().NotBeNull(); + + var clientId = resultDocument.ClientId!; - // await _mockAuthorServerPipeline.LoginAsync("bob"); + var dynamicIdp = _mockAuthorServerPipeline.ApplicationServices.GetRequiredService(); + dynamicIdp.Name = _mockIdPPipeline2.BaseUrl; + + ////////////////////// + // ClientAuthorize + ////////////////////// + + // Data Holder's Auth Server validates Identity Provider's Server software statement - var document = UdapDcrBuilderForAuthorizationCode - .Create(clientCert) - .WithAudience(UdapAuthServerPipeline.RegistrationEndpoint) - .WithExpiration(TimeSpan.FromMinutes(5)) - .WithJwtId() - .WithClientName("mock tiered test") - .WithLogoUri("https://example.com/logo.png") - .WithContacts(new HashSet + 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 { - "mailto:Joseph.Shook@Surescripts.com", "mailto:JoeShook@gmail.com" - }) - .WithTokenEndpointAuthMethod(UdapConstants.RegistrationDocumentValues.TokenEndpointAuthMethodValue) - .WithScope("udap openid user/*.read") - .WithResponseTypes(new List { "code" }) - .WithRedirectUrls(new List { "https://code_client/callback" }) - .Build(); + idp = "https://idpserver2?community=udap://idp-community-2" + }); + + _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://idpserver2?community=udap://idp-community-2"); + + // var schemes = await _mockAuthorServerPipeline.Resolve().GetAllSchemeNamesAsync(); + + + var sb = new StringBuilder(); + sb.Append("https://server/externallogin/challenge?"); // built in UdapAccount/Login/Index.cshtml.cs + sb.Append("scheme=").Append(TieredOAuthAuthenticationDefaults.AuthenticationScheme); + 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 - var signedSoftwareStatement = - SignedSoftwareStatementBuilder - .Create(clientCert, document) - .Build(); - - var requestBody = new UdapRegisterRequest - ( - signedSoftwareStatement, - UdapConstants.UdapVersionsSupportedValue, - new string[] { } + _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://idpserver2/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 = _mockIdPPipeline2.Clients.Single(c => c.ClientName == "AuthServer Client"); + idpClient.AlwaysIncludeUserClaimsInIdToken.Should().BeTrue(); + + _mockIdPPipeline2.BrowserClient.Should().NotBeNull(); + var backChannelAuthResult = await _mockIdPPipeline2.BrowserClient!.GetAsync(backChannelChallengeResponse.Headers.Location); + + + backChannelAuthResult.StatusCode.Should().Be(HttpStatusCode.Redirect, await backChannelAuthResult.Content.ReadAsStringAsync()); + // _testOutputHelper.WriteLine(backChannelAuthResult.Headers.Location!.AbsoluteUri); + backChannelAuthResult.Headers.Location!.AbsoluteUri.Should().StartWith("https://idpserver2/Account/Login"); + + // Run IdP /Account/Login + var loginCallbackResult = await _mockIdPPipeline2.BrowserClient.GetAsync(backChannelAuthResult.Headers.Location!.AbsoluteUri); + loginCallbackResult.StatusCode.Should().Be(HttpStatusCode.Redirect, await backChannelAuthResult.Content.ReadAsStringAsync()); + // _testOutputHelper.WriteLine(loginCallbackResult.Headers.Location!.OriginalString); + loginCallbackResult.Headers.Location!.OriginalString.Should().StartWith("/connect/authorize/callback?"); + + // Run IdP /connect/authorize/callback + var authorizeCallbackResult = await _mockIdPPipeline2.BrowserClient.GetAsync( + $"https://idpserver2{loginCallbackResult.Headers.Location!.OriginalString}"); + // _testOutputHelper.WriteLine(authorizeCallbackResult.Headers.Location!.OriginalString); + authorizeCallbackResult.StatusCode.Should().Be(HttpStatusCode.Redirect, await authorizeCallbackResult.Content.ReadAsStringAsync()); + authorizeCallbackResult.Headers.Location.Should().NotBeNull(); + authorizeCallbackResult.Headers.Location!.AbsoluteUri.Should().StartWith("https://server/federation/udap-tiered/signin?"); + + var backChannelCode = QueryHelpers.ParseQuery(authorizeCallbackResult.Headers.Location.Query).Single(p => p.Key == "code").Value.ToString(); + backChannelCode.Should().NotBeEmpty(); + + // + // Validate backchannel state is the same + // + backChannelState.Should().BeEquivalentTo(_mockAuthorServerPipeline.GetClientState(authorizeCallbackResult)); + + // + // Ensure client state and back channel state never become the same. + // + clientState.Should().NotBeEquivalentTo(backChannelState); + + _mockAuthorServerPipeline.GetSessionCookie().Should().BeNull(); + _mockAuthorServerPipeline.BrowserClient.GetCookie("https://server", "idsrv").Should().BeNull(); + + // Run Auth Server /federation/idpserver2/signin This is the Registered scheme callback endpoint + // Allow one redirect to run /connect/token. + // Sets Cookies: idsrv.external idsrv.session, and idsrv + // Backchannel calls: + // POST https://idpserver2/connect/token + // GET https://idpserver2/.well-known/openid-configuration + // GET https://idpserver2/.well-known/openid-configuration/jwks + // + // Redirects to https://server/externallogin/callback + // + + _mockAuthorServerPipeline.BrowserClient.AllowAutoRedirect = true; + _mockAuthorServerPipeline.BrowserClient.StopRedirectingAfter = 1; + _mockAuthorServerPipeline.BrowserClient.AllowCookies = true; + + + // "https://server/federation/idpserver2/signin?..." + var schemeCallbackResult = await _mockAuthorServerPipeline.BrowserClient.GetAsync(authorizeCallbackResult.Headers.Location!.AbsoluteUri); + + + schemeCallbackResult.StatusCode.Should().Be(HttpStatusCode.Redirect, await schemeCallbackResult.Content.ReadAsStringAsync()); + schemeCallbackResult.Headers.Location.Should().NotBeNull(); + schemeCallbackResult.Headers.Location!.OriginalString.Should().StartWith("/connect/authorize/callback?"); + // _testOutputHelper.WriteLine(schemeCallbackResult.Headers.Location!.OriginalString); + // Validate Cookies + _mockAuthorServerPipeline.GetSessionCookie().Should().NotBeNull(); + // _testOutputHelper.WriteLine(_mockAuthorServerPipeline.GetSessionCookie()!.Value); + // _mockAuthorServerPipeline.BrowserClient.GetCookie("https://server", "idsrv").Should().NotBeNull(); + //TODO assert match State and nonce between Auth Server and IdP + + // + // Check the IdToken in the back channel. Ensure the HL7_Identifier is in the claims + // + // _testOutputHelper.WriteLine(_mockIdPPipeline2.IdToken.ToString()); + + _mockIdPPipeline2.IdToken.Should().NotBeNull(); + _mockIdPPipeline2.IdToken!.Claims.Should().Contain(c => c.Type == "hl7_identifier"); + _mockIdPPipeline2.IdToken.Claims.Single(c => c.Type == "hl7_identifier").Value.Should().Be("123"); + + // Run the authServer https://server/connect/authorize/callback + _mockAuthorServerPipeline.BrowserClient.AllowAutoRedirect = false; + + var clientCallbackResult = await _mockAuthorServerPipeline.BrowserClient.GetAsync( + $"https://server{schemeCallbackResult.Headers.Location!.OriginalString}"); + + clientCallbackResult.StatusCode.Should().Be(HttpStatusCode.Redirect, await clientCallbackResult.Content.ReadAsStringAsync()); + clientCallbackResult.Headers.Location.Should().NotBeNull(); + clientCallbackResult.Headers.Location!.AbsoluteUri.Should().StartWith("https://code_client/callback?"); + // _testOutputHelper.WriteLine(clientCallbackResult.Headers.Location!.AbsoluteUri); + + + // Assert match state and nonce between User and Auth Server + clientState.Should().BeEquivalentTo(_mockAuthorServerPipeline.GetClientState(clientCallbackResult)); + + queryParams = QueryHelpers.ParseQuery(clientCallbackResult.Headers.Location.Query); + queryParams.Should().Contain(p => p.Key == "code"); + var code = queryParams.Single(p => p.Key == "code").Value.ToString(); + // _testOutputHelper.WriteLine($"Code: {code}"); + //////////////////////////// + // + // ClientAuthAccess + // + /////////////////////////// + + // Get a Access Token (Cash in the code) + + var privateCerts = _mockAuthorServerPipeline.Resolve(); + + var tokenRequest = AccessTokenRequestForAuthorizationCodeBuilder.Create( + clientId, + "https://server/connect/token", + privateCerts.IssuedCertificates.Select(ic => ic.Certificate).First(), + "https://code_client/callback", + code) + .Build(); + + + dynamicIdp.Name = null; // Influence UdapClient resolution in AddTieredOAuthForTests. + var udapClient = _mockAuthorServerPipeline.Resolve(); + + var accessToken = await udapClient.ExchangeCodeForTokenResponse(tokenRequest); + accessToken.Should().NotBeNull(); + accessToken.IdentityToken.Should().NotBeNull(); + var jwt = new JwtSecurityToken(accessToken.IdentityToken); + new JwtSecurityToken(accessToken.AccessToken).Should().NotBeNull(); + + + using var jsonDocument = JsonDocument.Parse(jwt.Payload.SerializeToJson()); + var formattedStatement = JsonSerializer.Serialize( + jsonDocument, + new JsonSerializerOptions { WriteIndented = true } ); - var response = await _mockAuthorServerPipeline.BrowserClient.PostAsync( - UdapAuthServerPipeline.RegistrationEndpoint, - new StringContent(JsonSerializer.Serialize(requestBody), new MediaTypeHeaderValue("application/json"))); + var formattedHeader = Base64UrlEncoder.Decode(jwt.EncodedHeader); - response.StatusCode.Should().Be(HttpStatusCode.Created); - var resultDocument = await response.Content.ReadFromJsonAsync(); + _testOutputHelper.WriteLine(formattedHeader); + _testOutputHelper.WriteLine(formattedStatement); + + + + // udap.org Tiered 4.3 + // aud: client_id of Resource Holder (matches client_id in Resource Holder request in Step 3.4) + jwt.Claims.Should().Contain(c => c.Type == "aud"); + jwt.Claims.Single(c => c.Type == "aud").Value.Should().Be(clientId); + + // iss: IdP’s unique identifying URI (matches idp parameter from Step 2) + jwt.Claims.Should().Contain(c => c.Type == "iss"); + jwt.Claims.Single(c => c.Type == "iss").Value.Should().Be(UdapAuthServerPipeline.BaseUrl); - return resultDocument; + jwt.Claims.Should().Contain(c => c.Type == "hl7_identifier"); + jwt.Claims.Single(c => c.Type == "hl7_identifier").Value.Should().Be("123"); + + + + + // sub: unique identifier for user in namespace of issuer, i.e. iss + sub is globally unique + + // TODO: Currently the sub is the code given at access time. Maybe that is OK? I could put the clientId in from + // backchannel. But I am not sure I want to show that. After all it is still globally unique. + // jwt.Claims.Should().Contain(c => c.Type == "sub"); + // jwt.Claims.Single(c => c.Type == "sub").Value.Should().Be(backChannelClientId); + + // jwt.Claims.Should().Contain(c => c.Type == "sub"); + // jwt.Claims.Single(c => c.Type == "sub").Value.Should().Be(backChannelCode); + + // Todo: Nonce + // Todo: Validate claims. Like missing name and other identity claims. Maybe add a hl7_identifier + // Why is idp:TieredOAuth in the returned claims? + + } + + private async Task RegisterClientWithAuthServer() + { + var clientCert = new X509Certificate2("CertStore/issued/fhirLabsApiClientLocalhostCert.pfx", "udap-test"); + + var udapClient = _mockAuthorServerPipeline.Resolve(); + + // + // Typically the client would validate a server before proceeding to registration. + // + udapClient.UdapServerMetaData = new UdapMetadata(new Mock().Object, new Mock>().Object) + { RegistrationEndpoint = UdapAuthServerPipeline.RegistrationEndpoint }; + + + var documentResponse = await udapClient.RegisterAuthCodeClient( + clientCert, + "udap openid user/*.read", + "https://server/udap.logo.48x48.png", + new List { "https://code_client/callback" }); + + documentResponse.GetError().Should().BeNull(); + + return documentResponse; } -} +} \ No newline at end of file diff --git a/_tests/UdapServer.Tests/Hl7RegistrationTests.cs b/_tests/UdapServer.Tests/Hl7RegistrationTests.cs index f5f4363b..cf52f34e 100644 --- a/_tests/UdapServer.Tests/Hl7RegistrationTests.cs +++ b/_tests/UdapServer.Tests/Hl7RegistrationTests.cs @@ -191,7 +191,7 @@ public async Task RegistrationSuccess_authorization_code_Test() IssuedAt = EpochTime.GetIntDate(now.ToUniversalTime()), JwtId = jwtId, ClientName = "udapTestClient", - LogoUri = "https://example.com/logo.png", + LogoUri = "https://avatars.githubusercontent.com/u/77421324?s=48&v=4", Contacts = new HashSet { "FhirJoe@BridgeTown.lab", "FhirJoe@test.lab" }, GrantTypes = new HashSet { "authorization_code", "refresh_token" }, ResponseTypes = new HashSet { "code" }, @@ -1246,7 +1246,7 @@ public async Task RegistrationInvalidClientMetadata_Invalid_GrantType_Test() IssuedAt = EpochTime.GetIntDate(now.ToUniversalTime()), JwtId = jwtId, ClientName = "udapTestClient", - LogoUri = "https://example.com/logo.png", + LogoUri = "https://avatars.githubusercontent.com/u/77421324?s=48&v=4", Contacts = new HashSet { "FhirJoe@BridgeTown.lab", "FhirJoe@test.lab" }, GrantTypes = new HashSet { "authorization_code", "refresh_bad" }, ResponseTypes = new HashSet { "code" }, @@ -1289,7 +1289,7 @@ public async Task RegistrationInvalidClientMetadata_Invalid_GrantType_Test() IssuedAt = EpochTime.GetIntDate(now.ToUniversalTime()), JwtId = jwtId, ClientName = "udapTestClient", - LogoUri = "https://example.com/logo.png", + LogoUri = "https://avatars.githubusercontent.com/u/77421324?s=48&v=4", Contacts = new HashSet { "FhirJoe@BridgeTown.lab", "FhirJoe@test.lab" }, GrantTypes = new HashSet { "refresh_bad" }, ResponseTypes = new HashSet { "code" }, @@ -1350,7 +1350,7 @@ public async Task RegistrationInvalidClientMetadata_responseTypesMissing_Test() IssuedAt = EpochTime.GetIntDate(now.ToUniversalTime()), JwtId = jwtId, ClientName = "udapTestClient", - LogoUri = "https://example.com/logo.png", + LogoUri = "https://avatars.githubusercontent.com/u/77421324?s=48&v=4", Contacts = new HashSet { "FhirJoe@BridgeTown.lab", "FhirJoe@test.lab" }, GrantTypes = new HashSet { "authorization_code" }, TokenEndpointAuthMethod = UdapConstants.RegistrationDocumentValues.TokenEndpointAuthMethodValue, diff --git a/_tests/UdapServer.Tests/IntegrationRegistrationTests.cs b/_tests/UdapServer.Tests/IntegrationRegistrationTests.cs index 66ffc06a..fd6cfa1f 100644 --- a/_tests/UdapServer.Tests/IntegrationRegistrationTests.cs +++ b/_tests/UdapServer.Tests/IntegrationRegistrationTests.cs @@ -30,6 +30,7 @@ using Udap.Model.Registration; using Udap.Model.Statement; using Udap.Server.Configuration; +using Udap.Server.Configuration.DependencyInjection; using Udap.Server.DbContexts; using Udap.Server.Options; using Udap.Server.Registration; @@ -313,7 +314,8 @@ public async Task GoodIUdapClientRegistrationStore() var builder = services.AddUdapServerBuilder(); - builder.AddUdapServerConfiguration(); + builder.AddUdapServerConfiguration() + .AddUdapInMemoryApiScopes(new List(){new ApiScope("system/Practitioner.read")}); services.AddIdentityServer(); @@ -366,7 +368,7 @@ public async Task GoodIUdapClientRegistrationStore() GrantTypes = new HashSet { "client_credentials" }, ResponseTypes = new HashSet { "authorization_code" }, TokenEndpointAuthMethod = UdapConstants.RegistrationDocumentValues.TokenEndpointAuthMethodValue, - Scope = "system/Patient.* system/Practitioner.read" + Scope = "system/Practitioner.read" }; diff --git a/_tests/UdapServer.Tests/UdapServer.Tests.csproj b/_tests/UdapServer.Tests/UdapServer.Tests.csproj index c399fd72..2d60d880 100644 --- a/_tests/UdapServer.Tests/UdapServer.Tests.csproj +++ b/_tests/UdapServer.Tests/UdapServer.Tests.csproj @@ -1,7 +1,7 @@  - net7.0 + net7.0 enable enable @@ -16,11 +16,23 @@ - + + + - + + PreserveNewest + true + PreserveNewest + + + PreserveNewest + true + PreserveNewest + + PreserveNewest true PreserveNewest @@ -106,18 +118,27 @@ Always + + Always + Always Always + + Always + PreserveNewest Always + + Always + diff --git a/_tests/UdapServer.Tests/Validators/UdapDCRValidatorTests.cs b/_tests/UdapServer.Tests/Validators/UdapDCRValidatorTests.cs index e712f34e..3c20d1f0 100644 --- a/_tests/UdapServer.Tests/Validators/UdapDCRValidatorTests.cs +++ b/_tests/UdapServer.Tests/Validators/UdapDCRValidatorTests.cs @@ -1,10 +1,21 @@ -using FluentAssertions; +#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 Duende.IdentityServer.Stores; +using FluentAssertions; using IdentityModel; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; -using Microsoft.IdentityModel.JsonWebTokens; using Microsoft.IdentityModel.Tokens; using Moq; +using Moq.Protected; using Udap.Common.Certificates; using Udap.Model; using Udap.Model.Registration; @@ -32,34 +43,44 @@ public DateTime UtcNow [Fact] public async Task ValidateLogo_Missing() { - var document = BuildUdapDcrValidator(out var validator); - - validator.ValidateLogoUri(document, out var errorResponse).Should().BeFalse(); - + var document = BuildUdapDcrValidator(GetHttpClientForLogo("image/png"), out var validator); + var (successFlag, errorResponse) = await validator.ValidateLogoUri(document); + successFlag.Should().BeFalse(); errorResponse.Should().NotBeNull(); errorResponse!.Error.Should().Be(UdapDynamicClientRegistrationErrors.InvalidClientMetadata); errorResponse.ErrorDescription.Should().Be($"{UdapDynamicClientRegistrationErrorDescriptions.LogoMissing}"); } [Fact] - public async Task ValidateLogo_InvalidFileType() + public async Task ValidateLogo_ValidContentType() { - var document = BuildUdapDcrValidator(out var validator); - document.LogoUri = "https://localhost/logo"; - validator.ValidateLogoUri(document, out var errorResponse).Should().BeFalse(); + var document = BuildUdapDcrValidator(GetHttpClientForLogo("image/png"), out var validator); + document.LogoUri = "https://avatars.githubusercontent.com/u/77421324?s=48&v=4"; + var (successFlag, errorResponse) = await validator.ValidateLogoUri(document); + successFlag.Should().BeTrue(); + errorResponse.Should().BeNull(); + } + + [Fact] + public async Task ValidateLogo_InvalidContentType() + { + var document = BuildUdapDcrValidator(GetHttpClientForLogo("image/tiff"), out var validator); + document.LogoUri = "https://localhost/logo"; + var (successFlag, errorResponse) = await validator.ValidateLogoUri(document); + successFlag.Should().BeFalse(); errorResponse.Should().NotBeNull(); errorResponse!.Error.Should().Be(UdapDynamicClientRegistrationErrors.InvalidClientMetadata); - errorResponse.ErrorDescription.Should().Be($"{UdapDynamicClientRegistrationErrorDescriptions.LogoInvalidFileType}"); + errorResponse.ErrorDescription.Should().Be($"{UdapDynamicClientRegistrationErrorDescriptions.LogoInvalidContentType}"); } [Fact] public async Task ValidateLogo_InvalidScheme() { - var document = BuildUdapDcrValidator(out var validator); + var document = BuildUdapDcrValidator(GetHttpClientForLogo("image/png"), out var validator); document.LogoUri = "http://localhost/logo.png"; - validator.ValidateLogoUri(document, out var errorResponse).Should().BeFalse(); - + var (successFlag, errorResponse) = await validator.ValidateLogoUri(document); + successFlag.Should().BeFalse(); errorResponse.Should().NotBeNull(); errorResponse!.Error.Should().Be(UdapDynamicClientRegistrationErrors.InvalidClientMetadata); errorResponse.ErrorDescription.Should().Be($"{UdapDynamicClientRegistrationErrorDescriptions.LogoInvalidScheme}"); @@ -68,10 +89,10 @@ public async Task ValidateLogo_InvalidScheme() [Fact] public async Task ValidateLogo_InvalidUri() { - var document = BuildUdapDcrValidator(out var validator); + var document = BuildUdapDcrValidator(GetHttpClientForLogo("image/png"), out var validator); document.LogoUri = "http:/localhost/logo.png"; // missing a slash - validator.ValidateLogoUri(document, out var errorResponse).Should().BeFalse(); - + var (successFlag, errorResponse) = await validator.ValidateLogoUri(document); + successFlag.Should().BeFalse(); errorResponse.Should().NotBeNull(); errorResponse!.Error.Should().Be(UdapDynamicClientRegistrationErrors.InvalidClientMetadata); errorResponse.ErrorDescription.Should().Be($"{UdapDynamicClientRegistrationErrorDescriptions.LogoInvalidUri}"); @@ -110,12 +131,30 @@ public async Task ValidateJti_And_ReplayTest() _clock.UtcNowFunc = () => UtcNow; + var mockHandler = new Mock(MockBehavior.Strict); + mockHandler.Protected() + .Setup>( + "SendAsync", + ItExpr.IsAny(), + ItExpr.IsAny() + ) + .ReturnsAsync((HttpRequestMessage request, CancellationToken token) => + { + HttpResponseMessage response = new HttpResponseMessage(); + + return response; + }); + + var httpClient = new HttpClient(mockHandler.Object); + var validator = new UdapDynamicClientRegistrationValidator( new Mock(new Mock>().Object).Object, + httpClient, new TestReplayCache(_clock), serverSettings, mockHttpContextAccessor.Object, new DefaultScopeExpander(), + new Mock().Object, new Mock>().Object); @@ -158,6 +197,7 @@ public async Task ValidateJti_And_ReplayTest() private static UdapDynamicClientRegistrationDocument BuildUdapDcrValidator( + HttpClient httpClient, out UdapDynamicClientRegistrationValidator validator) { var _clock = new StubClock(); @@ -189,16 +229,41 @@ private static UdapDynamicClientRegistrationDocument BuildUdapDcrValidator( context.Request.Host = new HostString("localhost:5001"); context.Request.Path = "/connect/register"; mockHttpContextAccessor.Setup(_ => _.HttpContext).Returns(context); - + validator = new UdapDynamicClientRegistrationValidator( new Mock(new Mock>().Object).Object, + httpClient, new TestReplayCache(_clock), serverSettings, mockHttpContextAccessor.Object, new DefaultScopeExpander(), + new Mock().Object, new Mock>().Object); return document; } + + private HttpClient GetHttpClientForLogo(string? contentType) + { + var mockHandler = new Mock(MockBehavior.Strict); + mockHandler.Protected() + .Setup>( + "SendAsync", + ItExpr.IsAny(), + ItExpr.IsAny() + ) + .ReturnsAsync((HttpRequestMessage request, CancellationToken token) => + { + HttpResponseMessage response = new HttpResponseMessage(HttpStatusCode.OK); + if (contentType != null) + { + response.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(contentType); + } + + return response; + }); + + return new HttpClient(mockHandler.Object); + } } public static class TestValidationExtensions{ diff --git a/_tests/UdapServer.Tests/appsettings.Auth.json b/_tests/UdapServer.Tests/appsettings.Auth.json new file mode 100644 index 00000000..f3c12556 --- /dev/null +++ b/_tests/UdapServer.Tests/appsettings.Auth.json @@ -0,0 +1,45 @@ +{ + "ConnectionStrings": { + "DefaultConnection": "Data Source=Udap.Sqlite.db;" + }, + + + "UdapFileCertStoreManifest": { + "Communities": [ + { + "Name": "udap://idp-community-1", + "Anchors": [ + { + "FilePath": "CertStore/anchors/caLocalhostCert.cer" + } + ], + "Intermediates": [ + "CertStore/intermediates/intermediateLocalhostCert.cer" + ], + "IssuedCerts": [ + { + "FilePath": "CertStore/issued/idpserver.pfx", + "Password": "udap-test" + } + ] + }, + { + "Name": "udap://idp-community-2", + "Anchors": [ + { + "FilePath": "CertStore/anchors/caLocalhostCert2.cer" + } + ], + "Intermediates": [ + "CertStore/intermediates/intermediateLocalhostCert2.cer" + ], + "IssuedCerts": [ + { + "FilePath": "CertStore/issued/idpserver2.pfx", + "Password": "udap-test" + } + ] + } + ] + } +} \ No newline at end of file diff --git a/_tests/UdapServer.Tests/appsettings.json b/_tests/UdapServer.Tests/appsettings.Idp1.json similarity index 91% rename from _tests/UdapServer.Tests/appsettings.json rename to _tests/UdapServer.Tests/appsettings.Idp1.json index d995be40..af8682e1 100644 --- a/_tests/UdapServer.Tests/appsettings.json +++ b/_tests/UdapServer.Tests/appsettings.Idp1.json @@ -1,8 +1,4 @@ { - "ConnectionStrings": { - "DefaultConnection": "Data Source=Udap.Sqlite.db;" - }, - "UdapMetadataOptions": { "Enabled": true, @@ -40,7 +36,6 @@ "Communities": [ { "Name": "udap://idp-community-1", - "IdPBaseUrl": "https://idpserver", "Anchors": [ { "FilePath": "CertStore/anchors/caLocalhostCert.cer" @@ -58,7 +53,6 @@ }, { "Name": "udap://idp-community-2", - "IdPBaseUrl": "https://idpserver", "Anchors": [ { "FilePath": "CertStore/anchors/caLocalhostCert.cer" diff --git a/_tests/UdapServer.Tests/appsettings.Idp2.json b/_tests/UdapServer.Tests/appsettings.Idp2.json new file mode 100644 index 00000000..62c61c5a --- /dev/null +++ b/_tests/UdapServer.Tests/appsettings.Idp2.json @@ -0,0 +1,47 @@ +{ + + "UdapMetadataOptions": { + "Enabled": true, + + "UdapProfilesSupported": [ + "udap_dcr", + "udap_authn", + "udap_authz", + "udap_to" + ], + + + "UdapMetadataConfigs": [ + { + "Community": "udap://idp-community-2", + "SignedMetadataConfig": { + "AuthorizationEndPoint": "https://idpserver2/connect/authorize", + "TokenEndpoint": "https://idpserver2/connect/token", + "RegistrationEndpoint": "https://idpserver2/connect/register" + } + } + ] + }, + + "UdapFileCertStoreManifest": { + "Communities": [ + { + "Name": "udap://idp-community-2", + "Anchors": [ + { + "FilePath": "CertStore/anchors/caLocalhostCert2.cer" + } + ], + "Intermediates": [ + "CertStore/intermediates/intermediateLocalhostCert2.cer" + ], + "IssuedCerts": [ + { + "FilePath": "CertStore/issued/idpserver2.pfx", + "Password": "udap-test" + } + ] + } + ] + } +} \ No newline at end of file diff --git a/artwork/udap.logo.48x48-removebg-preview.png b/artwork/udap.logo.48x48-removebg-preview.png new file mode 100644 index 00000000..7b335f4c Binary files /dev/null and b/artwork/udap.logo.48x48-removebg-preview.png differ diff --git a/artwork/udap.logo.48x48.png b/artwork/udap.logo.48x48.png new file mode 100644 index 00000000..9a351001 Binary files /dev/null and b/artwork/udap.logo.48x48.png differ diff --git a/examples/Directory.Packages.props b/examples/Directory.Packages.props deleted file mode 100644 index acb7a01b..00000000 --- a/examples/Directory.Packages.props +++ /dev/null @@ -1,30 +0,0 @@ - - - true - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/examples/FhirLabsApi/Dockerfile.gcp b/examples/FhirLabsApi/Dockerfile.gcp index 7f5734ac..8330ef9e 100644 --- a/examples/FhirLabsApi/Dockerfile.gcp +++ b/examples/FhirLabsApi/Dockerfile.gcp @@ -29,13 +29,13 @@ WORKDIR /app # Install system dependencies +ENV GCSFUSE_VERSION=1.2.0 + RUN set -e; \ - apt-get update -y && apt-get install -y gnupg2 tini lsb-release curl; \ - gcsFuseRepo=gcsfuse-`lsb_release -c -s`; \ - echo "deb http://packages.cloud.google.com/apt gcsfuse-bullseye main" | tee /etc/apt/sources.list.d/gcsfuse.list; \ - curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add -; \ - apt-get update; \ - apt-get install -y gcsfuse && apt-get clean + apt-get update -y && apt-get install -y gnupg2 tini fuse lsb-release curl; \ + curl -LJO "https://github.com/GoogleCloudPlatform/gcsfuse/releases/download/v${GCSFUSE_VERSION}/gcsfuse_${GCSFUSE_VERSION}_amd64.deb"; \ + apt-get install -y gcsfuse && apt-get clean; \ + dpkg -i "gcsfuse_${GCSFUSE_VERSION}_amd64.deb" ENV MNT_DIR=/mnt/gcs diff --git a/examples/FhirLabsApi/FhirLabsApi.csproj b/examples/FhirLabsApi/FhirLabsApi.csproj index 78448258..0916f393 100644 --- a/examples/FhirLabsApi/FhirLabsApi.csproj +++ b/examples/FhirLabsApi/FhirLabsApi.csproj @@ -1,4 +1,4 @@ - + net7.0 @@ -47,8 +47,8 @@ - - + + diff --git a/examples/FhirLabsApi/Properties/launchSettings.json b/examples/FhirLabsApi/Properties/launchSettings.json index b855a55a..1efb251a 100644 --- a/examples/FhirLabsApi/Properties/launchSettings.json +++ b/examples/FhirLabsApi/Properties/launchSettings.json @@ -4,7 +4,7 @@ "commandName": "Project", "launchBrowser": true, "launchUrl": "fhir/r4/.well-known/udap/communities/ashtml", - "applicationUrl": "https://localhost:7016;http://localhost:5016", + "applicationUrl": "https://host.docker.internal:7016;http://host.docker.internal:5016", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, 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 1186ee3a..3f12f335 100644 --- a/examples/Udap.Auth.Server.Admin/Udap.Auth.Server.Admin.csproj +++ b/examples/Udap.Auth.Server.Admin/Udap.Auth.Server.Admin.csproj @@ -14,11 +14,11 @@ - - - + + + - + diff --git a/examples/Udap.Auth.Server/HostingExtensions.cs b/examples/Udap.Auth.Server/HostingExtensions.cs index 6f1a11bc..7ce51897 100644 --- a/examples/Udap.Auth.Server/HostingExtensions.cs +++ b/examples/Udap.Auth.Server/HostingExtensions.cs @@ -13,6 +13,7 @@ using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.HttpOverrides; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; using OpenTelemetry.Resources; using OpenTelemetry.Trace; using Serilog; @@ -20,8 +21,11 @@ using Udap.Client.Configuration; using Udap.Common; using Udap.Server.Configuration; +using Udap.Server.Configuration.DependencyInjection; using Udap.Server.DbContexts; +using Udap.Server.Hosting.DynamicProviders.Oidc; using Udap.Server.Security.Authentication.TieredOAuth; +using Udap.Server.Stores; namespace Udap.Auth.Server; @@ -60,43 +64,43 @@ public static WebApplication ConfigureServices(this WebApplicationBuilder builde builder.Services.Configure(builder.Configuration.GetSection("UdapClientOptions")); builder.Services.AddUdapServer( - options => - { - var udapServerOptions = builder.Configuration.GetOption("ServerSettings"); - options.DefaultSystemScopes = udapServerOptions.DefaultSystemScopes; - options.DefaultUserScopes = udapServerOptions.DefaultUserScopes; - options.ServerSupport = udapServerOptions.ServerSupport; - options.ForceStateParamOnAuthorizationCode = udapServerOptions.ForceStateParamOnAuthorizationCode; - options.IdPMappings = udapServerOptions.IdPMappings; - options.LogoRequired = udapServerOptions.LogoRequired; - }, - // udapClientOptions => - // { - // var appSettings = builder.Configuration.GetOption("UdapClientOptions"); - // udapClientOptions.ClientName = "Udap.Auth.SecuredControls"; - // udapClientOptions.Contacts = new HashSet - // { "mailto:Joseph.Shook@Surescripts.com", "mailto:JoeShook@gmail.com" }; - // udapClientOptions.Headers = appSettings.Headers; - // }, - storeOptionAction: options => - _ = provider switch + options => { - "Sqlite" => options.UdapDbContext = b => - b.UseSqlite(connectionString, - dbOpts => dbOpts.MigrationsAssembly(typeof(Program).Assembly.FullName)), + 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; + }, + // udapClientOptions => + // { + // var appSettings = builder.Configuration.GetOption("UdapClientOptions"); + // udapClientOptions.ClientName = "Udap.Auth.SecuredControls"; + // udapClientOptions.Contacts = new HashSet + // { "mailto:Joseph.Shook@Surescripts.com", "mailto:JoeShook@gmail.com" }; + // udapClientOptions.Headers = appSettings.Headers; + // }, + storeOptionAction: options => + _ = provider switch + { + "Sqlite" => options.UdapDbContext = b => + b.UseSqlite(connectionString, + dbOpts => dbOpts.MigrationsAssembly(typeof(Program).Assembly.FullName)), - "SqlServer" => options.UdapDbContext = b => - b.UseSqlServer(connectionString, - dbOpts => dbOpts.MigrationsAssembly(typeof(Program).Assembly.FullName)), + "SqlServer" => options.UdapDbContext = b => + b.UseSqlServer(connectionString, + dbOpts => dbOpts.MigrationsAssembly(typeof(Program).Assembly.FullName)), + + _ => throw new Exception($"Unsupported provider: {provider}") + }) + .AddUdapResponseGenerators() + .AddSmartV2Expander(); - _ => throw new Exception($"Unsupported provider: {provider}") - }) - .AddUdapResponseGenerators(); builder.Services.Configure(builder.Configuration.GetSection(Common.Constants.UDAP_FILE_STORE_MANIFEST)); - builder.Services.AddIdentityServer(options => { @@ -141,98 +145,24 @@ public static WebApplication ConfigureServices(this WebApplicationBuilder builde .AddClientStore() //TODO remove .AddTestUsers(TestUsers.Users); - - + // .AddIdentityProviderStore(); // last to register wins. Uhg! - //TODO: Hack for connectionathon for the time being + // + // Don't cache in this example project. It can hide bugs such as the dynamic UDAP Tiered OAuth Provider + // options properties as the OIDC handshake bounces from machine to machine. When caching is enabled + // TieredOAuthOptions are retained even after the redirect. This works until you are scaled up. + // So best to not cache so we can catch logic errors in integration testing. + // + // .AddInMemoryCaching() + // .AddIdentityProviderStoreCache(); // last to register wins. Uhg! - if (Environment.GetEnvironmentVariable("GCPDeploy") == "true") - { - builder.Services.AddAuthentication() - // - // By convention the scheme name should match the community name in UdapFileCertStoreManifest - // to allow discovery of the IdPBaseUrl - // - .AddTieredOAuth(options => - { - options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme; - options.AuthorizationEndpoint = "https://idp1.securedcontrols.net/connect/authorize"; - options.TokenEndpoint = "https://idp1.securedcontrols.net/connect/token"; - options.IdPBaseUrl = "https://idp1.securedcontrols.net"; - }) - .AddTieredOAuth("TieredOAuthProvider2", "UDAP Tiered OAuth (DOTNET-Provider2)", options => - { - options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme; - options.AuthorizationEndpoint = "https://idp2.securedcontrols.net/connect/authorize"; - options.TokenEndpoint = "https://idp2.securedcontrols.net/connect/token"; - options.CallbackPath = "/signin-tieredoauthprovider2"; - options.IdPBaseUrl = "https://idp2.securedcontrols.net"; - }) - .AddTieredOAuth("OktaForUDAP", "UDAP Tiered OAuth Okta", options => - { - options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme; - options.AuthorizationEndpoint = "https://udap.zimt.work/oauth2/aus5wvee13EWm169M1d7/v1/authorize"; - options.TokenEndpoint = "https://udap.zimt.work/oauth2/aus5wvee13EWm169M1d7/v1/token"; - options.CallbackPath = "/signin-oktaforudap"; - options.IdPBaseUrl = "https://udap.zimt.work/oauth2/aus5wvee13EWm169M1d7"; - }); - } - else - { - builder.Services.AddAuthentication() - // - // By convention the scheme name should match the community name in UdapFileCertStoreManifest - // to allow discovery of the IdPBaseUrl - // + builder.Services.AddAuthentication() .AddTieredOAuth(options => { options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme; - //TODO Get AuthorizationEndpoint from IdpBaseUrl Udap Metadata - options.AuthorizationEndpoint = "https://host.docker.internal:5055/connect/authorize"; - //options.TokenEndpoint = "Get from UDAP metadata - options.TokenEndpoint = "https://host.docker.internal:5055/connect/token"; - // options.ClientId = "dynamic"; - // options.Events.OnRedirectToAuthorizationEndpoint - // { - // - // }; - options.IdPBaseUrl = "https://host.docker.internal:5055"; - }) - .AddTieredOAuth("TieredOAuthProvider2", "UDAP Tiered OAuth (DOTNET-Provider2)", options => - { - options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme; - //TODO Get AuthorizationEndpoint from IdpBaseUrl Udap Metadata - options.AuthorizationEndpoint = "https://host.docker.internal:5057/connect/authorize"; - options.TokenEndpoint = "https://host.docker.internal:5057/connect/token"; - // - // When repeating AddTieredOAuth extension always add set a unique CallbackPath - // Otherwise the following error will occur: "The oauth state was missing or invalid." - // - // Buried in asp.net RemoteAuthenticationHandler.cs the following code decides on what scheme - // to use during HandleRequestAsync() by the CallbackPath registered - // - // deciding code in RemoteAuthenticationHandler.cs: - // public virtual Task ShouldHandleRequestAsync() - // => Task.FromResult(Options.CallbackPath == Request.Path); - // - options.CallbackPath = "/signin-tieredoauthprovider2"; - options.IdPBaseUrl = "https://host.docker.internal:5057"; - }) - .AddTieredOAuth("OktaForUDAP", "UDAP Tiered OAuth Okta", options => - { - options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme; - //TODO Get AuthorizationEndpoint from IdpBaseUrl Udap Metadata - options.AuthorizationEndpoint = "https://udap.zimt.work/oauth2/aus5wvee13EWm169M1d7/v1/authorize"; - //options.TokenEndpoint = "Get from UDAP metadata - options.TokenEndpoint = "https://udap.zimt.work/oauth2/aus5wvee13EWm169M1d7/v1/token"; - options.CallbackPath = "/signin-oktaforudap"; - options.IdPBaseUrl = "https://udap.zimt.work/oauth2/aus5wvee13EWm169M1d7"; - }); - } - - + builder.Services.AddSingleton(); // diff --git a/examples/Udap.Auth.Server/Pages/Account/Login/Index.cshtml b/examples/Udap.Auth.Server/Pages/Account/Login/Index.cshtml index d3b70046..9003cb60 100644 --- a/examples/Udap.Auth.Server/Pages/Account/Login/Index.cshtml +++ b/examples/Udap.Auth.Server/Pages/Account/Login/Index.cshtml @@ -67,7 +67,7 @@ + asp-route-returnUrl="@Model.Input.ReturnUrl"> @provider.DisplayName diff --git a/examples/Udap.Auth.Server/Pages/Account/Login/Index.cshtml.cs b/examples/Udap.Auth.Server/Pages/Account/Login/Index.cshtml.cs index 29841e95..da4149f4 100644 --- a/examples/Udap.Auth.Server/Pages/Account/Login/Index.cshtml.cs +++ b/examples/Udap.Auth.Server/Pages/Account/Login/Index.cshtml.cs @@ -8,7 +8,6 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; -using Udap.Server.Configuration; namespace Udap.Auth.Server.Pages.Account.Login; @@ -19,7 +18,6 @@ public class Index : PageModel private readonly TestUserStore _users; private readonly IIdentityServerInteractionService _interaction; private readonly IEventService _events; - private readonly ServerSettings _serverSettings; private readonly IAuthenticationSchemeProvider _schemeProvider; private readonly IIdentityProviderStore _identityProviderStore; @@ -33,7 +31,6 @@ public Index( IAuthenticationSchemeProvider schemeProvider, IIdentityProviderStore identityProviderStore, IEventService events, - ServerSettings serverSettings, TestUserStore users = null) { // this is where you would plug in your own custom identity management library (e.g. ASP.NET Identity) @@ -43,7 +40,6 @@ public Index( _schemeProvider = schemeProvider; _identityProviderStore = identityProviderStore; _events = events; - _serverSettings = serverSettings; } public async Task OnGet(string returnUrl) @@ -165,8 +161,6 @@ private async Task BuildModelAsync(string returnUrl) }; var context = await _interaction.GetAuthorizationContextAsync(returnUrl); - - // NOTE:: This if statement concerning IdP is not the same as the UDAP IdP. if (context?.IdP != null && await _schemeProvider.GetSchemeAsync(context.IdP) != null) { var local = context.IdP == Duende.IdentityServer.IdentityServerConstants.LocalIdentityProvider; @@ -188,26 +182,23 @@ private async Task BuildModelAsync(string returnUrl) } var schemes = await _schemeProvider.GetAllSchemesAsync(); - var providers = schemes .Where(x => x.DisplayName != null) .Select(x => new ViewModel.ExternalProvider { DisplayName = x.DisplayName ?? x.Name, - AuthenticationScheme = x.Name, - ReturnUrl = LoadReturnUrl(x, returnUrl) + AuthenticationScheme = x.Name }).ToList(); - var dynamicSchemes = (await _identityProviderStore.GetAllSchemeNamesAsync()) + var dyanmicSchemes = (await _identityProviderStore.GetAllSchemeNamesAsync()) .Where(x => x.Enabled) .Select(x => new ViewModel.ExternalProvider { AuthenticationScheme = x.Scheme, - DisplayName = x.DisplayName, - ReturnUrl = returnUrl + DisplayName = x.DisplayName }); - providers.AddRange(dynamicSchemes); + providers.AddRange(dyanmicSchemes); var allowLocal = true; @@ -228,21 +219,4 @@ private async Task BuildModelAsync(string returnUrl) ExternalProviders = providers.ToArray() }; } - - private string LoadReturnUrl(AuthenticationScheme authenticationScheme, string returnUrl) - { - if (_serverSettings.IdPMappings != null && _serverSettings.IdPMappings.Any()) - { - var idpBaseUrl = _serverSettings.IdPMappings - .FirstOrDefault(x => x.Scheme == authenticationScheme.Name) - ?.IdpBaseUrl; - - if(string.IsNullOrEmpty(idpBaseUrl)) - return returnUrl; - - return $"{returnUrl}&idp={idpBaseUrl}"; - } - - return returnUrl; - } } \ No newline at end of file diff --git a/examples/Udap.Auth.Server/Pages/Shared/_Nav.cshtml b/examples/Udap.Auth.Server/Pages/Shared/_Nav.cshtml index c43dd396..93943df7 100644 --- a/examples/Udap.Auth.Server/Pages/Shared/_Nav.cshtml +++ b/examples/Udap.Auth.Server/Pages/Shared/_Nav.cshtml @@ -11,10 +11,10 @@ diff --git a/examples/Udap.Auth.Server/Pages/TestUsers.cs b/examples/Udap.Auth.Server/Pages/TestUsers.cs index 6152b53e..33baf691 100644 --- a/examples/Udap.Auth.Server/Pages/TestUsers.cs +++ b/examples/Udap.Auth.Server/Pages/TestUsers.cs @@ -29,7 +29,7 @@ public static List Users new TestUser { SubjectId = "1", - Username = "alice", + Username = "alicenewman@example.com", Password = "alice", Claims = { diff --git a/examples/Udap.Auth.Server/Pages/UdapAccount/Login/Index.cshtml b/examples/Udap.Auth.Server/Pages/UdapAccount/Login/Index.cshtml index ee064a5b..637257b6 100644 --- a/examples/Udap.Auth.Server/Pages/UdapAccount/Login/Index.cshtml +++ b/examples/Udap.Auth.Server/Pages/UdapAccount/Login/Index.cshtml @@ -9,7 +9,7 @@ -
+
@if (Model.View.EnableLocalLogin) { @@ -63,10 +63,8 @@
    @foreach (var provider in Model.View.VisibleExternalProviders) { - var buttonClass = provider.IsChosenIdp ? "btn-primary" : "btn-secondary"; -
  • - @@ -88,4 +86,26 @@
}
+ \ No newline at end of file diff --git a/examples/Udap.Auth.Server/Pages/UdapAccount/Login/Index.cshtml.cs b/examples/Udap.Auth.Server/Pages/UdapAccount/Login/Index.cshtml.cs index 74f994b1..06448d78 100644 --- a/examples/Udap.Auth.Server/Pages/UdapAccount/Login/Index.cshtml.cs +++ b/examples/Udap.Auth.Server/Pages/UdapAccount/Login/Index.cshtml.cs @@ -1,4 +1,12 @@ -using System.Web; +#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 Duende.IdentityServer; using Duende.IdentityServer.Events; using Duende.IdentityServer.Models; @@ -10,7 +18,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.AspNetCore.WebUtilities; -using Udap.Server.Configuration; +using System.Web; namespace Udap.Auth.Server.Pages.UdapAccount.Login; @@ -21,22 +29,20 @@ public class Index : PageModel private readonly TestUserStore _users; private readonly IIdentityServerInteractionService _interaction; private readonly IEventService _events; - private readonly ServerSettings _serverSettings; private readonly IAuthenticationSchemeProvider _schemeProvider; private readonly IIdentityProviderStore _identityProviderStore; - public ViewModel View { get; set; } + public ViewModel? View { get; set; } [BindProperty] - public InputModel Input { get; set; } + public InputModel? Input { get; set; } public Index( IIdentityServerInteractionService interaction, IAuthenticationSchemeProvider schemeProvider, IIdentityProviderStore identityProviderStore, IEventService events, - ServerSettings serverSettings, - TestUserStore users = null) + TestUserStore users) { // this is where you would plug in your own custom identity management library (e.g. ASP.NET Identity) _users = users ?? throw new Exception("Please call 'AddTestUsers(TestUsers.Users)' on the IIdentityServerBuilder in Startup or remove the TestUserStore from the AccountController."); @@ -45,14 +51,13 @@ public Index( _schemeProvider = schemeProvider; _identityProviderStore = identityProviderStore; _events = events; - _serverSettings = serverSettings; } public async Task OnGet(string returnUrl) { await BuildModelAsync(returnUrl); - if (View.IsExternalLoginOnly) + if (View != null && View.IsExternalLoginOnly) { // we only have one option for logging in and it's an external provider return RedirectToPage("/UdapTieredLogin/Challenge", new { scheme = View.ExternalLoginScheme, returnUrl }); @@ -63,6 +68,8 @@ public async Task OnGet(string returnUrl) public async Task OnPost() { + if (Input == null) throw new InvalidOperationException("Input is null"); + // check if we are in the context of an authorization request var context = await _interaction.GetAuthorizationContextAsync(Input.ReturnUrl); @@ -103,7 +110,7 @@ public async Task OnPost() // only set explicit expiration here if user chooses "remember me". // otherwise we rely upon expiration configured in cookie middleware. - AuthenticationProperties props = null; + AuthenticationProperties? props = null; if (LoginOptions.AllowRememberLogin && Input.RememberLogin) { props = new AuthenticationProperties @@ -111,15 +118,15 @@ public async Task OnPost() IsPersistent = true, ExpiresUtc = DateTimeOffset.UtcNow.Add(LoginOptions.RememberMeLoginDuration) }; - }; + } // issue authentication cookie with subject ID and username - var isuser = new IdentityServerUser(user.SubjectId) + var issuer = new IdentityServerUser(user.SubjectId) { DisplayName = user.Username }; - await HttpContext.SignInAsync(isuser, props); + await HttpContext.SignInAsync(issuer, props); if (context != null) { @@ -172,7 +179,7 @@ private async Task BuildModelAsync(string returnUrl) // TODO: Well... this could be... need to revisit if (context?.IdP != null && await _schemeProvider.GetSchemeAsync(context.IdP) != null) { - var local = context.IdP == Duende.IdentityServer.IdentityServerConstants.LocalIdentityProvider; + var local = context.IdP == IdentityServerConstants.LocalIdentityProvider; // this is meant to short circuit the UI and only trigger the one external IdP View = new ViewModel @@ -180,7 +187,7 @@ private async Task BuildModelAsync(string returnUrl) EnableLocalLogin = local, }; - Input.Username = context?.LoginHint; + Input.Username = context.LoginHint ?? string.Empty; if (!local) { @@ -191,27 +198,27 @@ private async Task BuildModelAsync(string returnUrl) } var schemes = await _schemeProvider.GetAllSchemesAsync(); - - var providers = schemes .Where(x => x.DisplayName != null) .Select(x => { - var (enrichedReturnUrl, matchIdp) = LoadReturnUrl(x, returnUrl); - - var externalProvider = new ViewModel.ExternalProvider { DisplayName = x.DisplayName ?? x.Name, AuthenticationScheme = x.Name, - ReturnUrl = enrichedReturnUrl, - IsChosenIdp = matchIdp + ReturnUrl = returnUrl }; - + + if (QueryHelpers.ParseQuery(HttpUtility.UrlDecode(returnUrl)).TryGetValue("idp", out var udapIdp)) + { + externalProvider.TieredOAuthIdp = udapIdp.FirstOrDefault(); + } + return externalProvider; - }) - .ToList(); + + }).ToList(); + var dynamicSchemes = (await _identityProviderStore.GetAllSchemeNamesAsync()) .Where(x => x.Enabled) @@ -221,6 +228,7 @@ private async Task BuildModelAsync(string returnUrl) DisplayName = x.DisplayName, ReturnUrl = returnUrl }); + providers.AddRange(dynamicSchemes); @@ -231,7 +239,7 @@ private async Task BuildModelAsync(string returnUrl) allowLocal = client.EnableLocalLogin; if (client.IdentityProviderRestrictions != null && client.IdentityProviderRestrictions.Any()) { - providers = providers.Where(provider => client.IdentityProviderRestrictions.Contains(provider.AuthenticationScheme)).ToList(); + providers = providers.Where(provider => client.IdentityProviderRestrictions.Contains(provider.AuthenticationScheme!)).ToList(); } } @@ -242,44 +250,4 @@ private async Task BuildModelAsync(string returnUrl) ExternalProviders = providers.ToArray() }; } - - private (string, bool) LoadReturnUrl(AuthenticationScheme authenticationScheme, string returnUrl) - { - if (_serverSettings.IdPMappings != null && _serverSettings.IdPMappings.Any()) - { - var idpBaseUrl = _serverSettings.IdPMappings - .FirstOrDefault(x => x.Scheme == authenticationScheme.Name) - ?.IdpBaseUrl; - - if(string.IsNullOrEmpty(idpBaseUrl)) - return (returnUrl, false); - - if (QueryHelpers.ParseQuery(HttpUtility.UrlDecode(returnUrl)).TryGetValue("idp", out var udapIdp)) - { - if (udapIdp == idpBaseUrl) - { - return (returnUrl, true); - } - - var uri = new Uri(returnUrl, UriKind.Relative); - var uriParts = uri.OriginalString.Split('?'); - - if (uriParts.Length != 2) - { - throw new Exception("invalid return URL"); - } - - - var queryParams = QueryHelpers.ParseQuery(HttpUtility.UrlDecode(uriParts[1])); - queryParams.Remove("idp"); - var newReturnUrl = QueryHelpers.AddQueryString(uriParts[0], queryParams.ToDictionary(x => x.Key, x => x.Value.ToString())!); - - return ($"{newReturnUrl}&idp={idpBaseUrl}", false); - } - - return ($"{returnUrl}&idp={idpBaseUrl}", false); - } - - return (returnUrl, false); - } } \ No newline at end of file diff --git a/examples/Udap.Auth.Server/Pages/UdapAccount/Login/InputModel.cs b/examples/Udap.Auth.Server/Pages/UdapAccount/Login/InputModel.cs index 3329d4d8..5a27f5d3 100644 --- a/examples/Udap.Auth.Server/Pages/UdapAccount/Login/InputModel.cs +++ b/examples/Udap.Auth.Server/Pages/UdapAccount/Login/InputModel.cs @@ -5,14 +5,14 @@ namespace Udap.Auth.Server.Pages.UdapAccount.Login; public class InputModel { [Required] - public string Username { get; set; } - + public string Username { get; set; } = default!; + [Required] - public string Password { get; set; } - + public string Password { get; set; } = default!; + public bool RememberLogin { get; set; } - - public string ReturnUrl { get; set; } - public string Button { get; set; } + public string ReturnUrl { get; set; } = default!; + + public string Button { get; set; } = default!; } \ No newline at end of file diff --git a/examples/Udap.Auth.Server/Pages/UdapAccount/Login/ViewModel.cs b/examples/Udap.Auth.Server/Pages/UdapAccount/Login/ViewModel.cs index fa113034..5cf8d7f4 100644 --- a/examples/Udap.Auth.Server/Pages/UdapAccount/Login/ViewModel.cs +++ b/examples/Udap.Auth.Server/Pages/UdapAccount/Login/ViewModel.cs @@ -1,3 +1,6 @@ +using Microsoft.Extensions.Primitives; +using Udap.Server.Security.Authentication.TieredOAuth; + namespace Udap.Auth.Server.Pages.UdapAccount.Login; public class ViewModel @@ -5,19 +8,28 @@ public class ViewModel public bool AllowRememberLogin { get; set; } = true; public bool EnableLocalLogin { get; set; } = true; - public IEnumerable ExternalProviders { get; set; } = Enumerable.Empty(); - public IEnumerable VisibleExternalProviders => ExternalProviders.Where(x => !String.IsNullOrWhiteSpace(x.DisplayName)); + public IEnumerable ExternalProviders { get; set; } = Enumerable.Empty(); + + public IEnumerable VisibleExternalProviders => + ExternalProviders.Where(x => + !string.IsNullOrWhiteSpace(x.DisplayName) && + x.AuthenticationScheme != TieredOAuthAuthenticationDefaults.AuthenticationScheme); + + public ExternalProvider? TieredProvider => + ExternalProviders.SingleOrDefault(p => + !string.IsNullOrEmpty(p.TieredOAuthIdp) && + p.AuthenticationScheme == TieredOAuthAuthenticationDefaults.AuthenticationScheme); - public bool IsExternalLoginOnly => EnableLocalLogin == false && ExternalProviders?.Count() == 1; - public string ExternalLoginScheme => IsExternalLoginOnly ? ExternalProviders?.SingleOrDefault()?.AuthenticationScheme : null; + + public bool IsExternalLoginOnly => EnableLocalLogin == false && ExternalProviders.Count() == 1; + public string? ExternalLoginScheme => ExternalProviders.SingleOrDefault()?.AuthenticationScheme; public class ExternalProvider { - public string DisplayName { get; set; } - public string AuthenticationScheme { get; set; } - - public string ReturnUrl { get; set; } + public string? DisplayName { get; set; } + public string? AuthenticationScheme { get; set; } - public bool IsChosenIdp { get; set; } + public string? ReturnUrl { get; set; } + public string? TieredOAuthIdp { get; set; } } } \ No newline at end of file diff --git a/examples/Udap.Auth.Server/Pages/UdapTieredLogin/Challenge.cshtml.cs b/examples/Udap.Auth.Server/Pages/UdapTieredLogin/Challenge.cshtml.cs index 95b7170a..0df918c5 100644 --- a/examples/Udap.Auth.Server/Pages/UdapTieredLogin/Challenge.cshtml.cs +++ b/examples/Udap.Auth.Server/Pages/UdapTieredLogin/Challenge.cshtml.cs @@ -1,8 +1,18 @@ +#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 Duende.IdentityServer.Services; -using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; +using Udap.Client.Client; +using Udap.Server.Security.Authentication.TieredOAuth; namespace Udap.Auth.Server.Pages.UdapTieredLogin; @@ -11,35 +21,26 @@ namespace Udap.Auth.Server.Pages.UdapTieredLogin; public class Challenge : PageModel { private readonly IIdentityServerInteractionService _interactionService; + private readonly IUdapClient _udapClient; - public Challenge(IIdentityServerInteractionService interactionService) + public Challenge(IIdentityServerInteractionService interactionService, IUdapClient udapClient) { _interactionService = interactionService; + _udapClient = udapClient; } - public IActionResult OnGet(string scheme, string returnUrl) + public async Task OnGetAsync(string scheme, string returnUrl) { if (string.IsNullOrEmpty(returnUrl)) returnUrl = "~/"; + + var props = await TieredOAuthHelpers.BuildDynamicTieredOAuthOptions( + _interactionService, + _udapClient, + scheme, + "/udaptieredlogin/callback", + returnUrl); - // validate returnUrl - either it is a valid OIDC URL or back to a local page - if (Url.IsLocalUrl(returnUrl) == false && _interactionService.IsValidReturnUrl(returnUrl) == false) - { - // user might have clicked on a malicious link - should be logged - throw new Exception("invalid return URL"); - } - // start challenge and roundtrip the return URL and scheme - var props = new AuthenticationProperties - { - RedirectUri = Url.Page("/udaptieredlogin/callback"), - - Items = - { - { "returnUrl", returnUrl }, - { "scheme", scheme }, - } - }; - return Challenge(props, scheme); } } \ No newline at end of file diff --git a/examples/Udap.Auth.Server/Properties/launchSettings.json b/examples/Udap.Auth.Server/Properties/launchSettings.json index a2efbb28..01984f75 100644 --- a/examples/Udap.Auth.Server/Properties/launchSettings.json +++ b/examples/Udap.Auth.Server/Properties/launchSettings.json @@ -5,6 +5,7 @@ "launchBrowser": true, "environmentVariables": { "GCPDeploy": "false", + "ASPNETCORE_ENVIRONMENT": "Development", "UdapIdpBaseUrl": "https://host.docker.internal:5002" }, "sslPort": 5002, diff --git a/examples/Udap.Auth.Server/Udap.Auth.Server.csproj b/examples/Udap.Auth.Server/Udap.Auth.Server.csproj index c84bb3da..00e2e9b9 100644 --- a/examples/Udap.Auth.Server/Udap.Auth.Server.csproj +++ b/examples/Udap.Auth.Server/Udap.Auth.Server.csproj @@ -19,12 +19,12 @@ - + 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 5be1e984..8fc1515e 100644 --- a/examples/Udap.Auth.Server/appsettings.Development.json +++ b/examples/Udap.Auth.Server/appsettings.Development.json @@ -17,26 +17,13 @@ "Headers": { "USER_KEY": "hobojoe", "ORG_KEY": "travelOrg" - } + }, + "TieredOAuthClientLogo": "https://host.docker.internal:5002/udap.logo.48x48.png" }, "ServerSettings": { "ServerSupport": "Hl7SecurityIG", - "LogoRequired": "true", - "IdPMappings": [ - { - "Scheme": "TieredOAuth", - "IdpBaseUrl": "https://host.docker.internal:5055" - }, - { - "Scheme": "TieredOAuthProvider2", - "IdpBaseUrl": "https://host.docker.internal:5057" - }, - { - "Scheme": "OktaForUDAP", - "IdpBaseUrl": "https://udap.zimt.work/oauth2/aus5wvee13EWm169M1d7" - } - ] + "LogoRequired": "true" }, "ConnectionStrings": { @@ -47,7 +34,6 @@ "Communities": [ { "Name": "udap://TieredProvider1", - "IdPBaseUrl": "https://host.docker.internal:5055", "IssuedCerts": [ { "FilePath": "CertStore/issued/fhirLabsApiClientLocalhostCert.pfx", @@ -56,8 +42,7 @@ ] }, { - "Name": "udap://TieredProvider2", - "IdPBaseUrl": "https://host.docker.internal:5057", + "Name": "udap://Provider2", "IssuedCerts": [ { "FilePath": "CertStore/issued/fhirLabsApiClientLocalhostCert2.pfx", @@ -67,7 +52,6 @@ }, { "Name": "udap://Okta", - "IdPBaseUrl": "https://udap.zimt.work/oauth2/aus5wvee13EWm169M1d7", "IssuedCerts": [ { "FilePath": "CertStore/issued/udap-sandbox-surescripts-2.p12", diff --git a/examples/Udap.Auth.Server/appsettings.Production.json b/examples/Udap.Auth.Server/appsettings.Production.json index 250d2d5f..b243ff0e 100644 --- a/examples/Udap.Auth.Server/appsettings.Production.json +++ b/examples/Udap.Auth.Server/appsettings.Production.json @@ -27,21 +27,7 @@ //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, - "IdPMappings": [ - { - "Scheme": "TieredOAuth", - "IdpBaseUrl": "https://idp1.securedcontrols.net" - }, - { - "Scheme": "TieredOAuthProvider2", - "IdpBaseUrl": "https://idp2.securedcontrols.net" - }, - { - "Scheme": "OktaForUDAP", - "IdpBaseUrl": "https://udap.zimt.work/oauth2/aus5wvee13EWm169M1d7" - } - ] + "ForceStateParamOnAuthorizationCode": true }, "ConnectionStrings": { diff --git a/examples/Udap.Auth.Server/wwwroot/udap.logo.48x48.png b/examples/Udap.Auth.Server/wwwroot/udap.logo.48x48.png new file mode 100644 index 00000000..52f96f8f Binary files /dev/null and b/examples/Udap.Auth.Server/wwwroot/udap.logo.48x48.png differ diff --git a/examples/Udap.CA/Udap.CA.csproj b/examples/Udap.CA/Udap.CA.csproj index 5b2a4b74..866f37eb 100644 --- a/examples/Udap.CA/Udap.CA.csproj +++ b/examples/Udap.CA/Udap.CA.csproj @@ -13,13 +13,13 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/examples/Udap.Identity.Provider.2/Pages/Shared/_Nav.cshtml b/examples/Udap.Identity.Provider.2/Pages/Shared/_Nav.cshtml index 6d3edc2d..1ad3a0be 100644 --- a/examples/Udap.Identity.Provider.2/Pages/Shared/_Nav.cshtml +++ b/examples/Udap.Identity.Provider.2/Pages/Shared/_Nav.cshtml @@ -11,10 +11,10 @@ diff --git a/examples/Udap.Identity.Provider.2/Properties/launchSettings.json b/examples/Udap.Identity.Provider.2/Properties/launchSettings.json index 2a04814c..ce257019 100644 --- a/examples/Udap.Identity.Provider.2/Properties/launchSettings.json +++ b/examples/Udap.Identity.Provider.2/Properties/launchSettings.json @@ -4,11 +4,11 @@ "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": true, - "applicationUrl": "https://localhost:5057", + "applicationUrl": "https://host.docker.internal:5057", "environmentVariables": { "GCPDeploy": "false", "ASPNETCORE_ENVIRONMENT": "Development", - "UdapIdpBaseUrl": "https://localhost:5057" + "UdapIdpBaseUrl": "https://host.docker.internal:5057" }, "sslPort": 5057 }, 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 d2ff1240..9bac45f1 100644 --- a/examples/Udap.Identity.Provider.2/Udap.Identity.Provider.2.csproj +++ b/examples/Udap.Identity.Provider.2/Udap.Identity.Provider.2.csproj @@ -23,12 +23,12 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all - - + + diff --git a/examples/Udap.Identity.Provider.2/wwwroot/udap.logo.48x48.png b/examples/Udap.Identity.Provider.2/wwwroot/udap.logo.48x48.png new file mode 100644 index 00000000..52f96f8f Binary files /dev/null and b/examples/Udap.Identity.Provider.2/wwwroot/udap.logo.48x48.png differ diff --git a/examples/Udap.Identity.Provider/HostingExtensions.cs b/examples/Udap.Identity.Provider/HostingExtensions.cs index cfba1817..48832eae 100644 --- a/examples/Udap.Identity.Provider/HostingExtensions.cs +++ b/examples/Udap.Identity.Provider/HostingExtensions.cs @@ -1,4 +1,4 @@ -#region (c) 2022 Joseph Shook. All rights reserved. +#region (c) 2023 Joseph Shook. All rights reserved. // /* // Authors: // Joseph Shook Joseph.Shook@Surescripts.com @@ -12,11 +12,9 @@ using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.HttpOverrides; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Options; using OpenTelemetry.Resources; using OpenTelemetry.Trace; using Udap.Common; -using Udap.Common.Certificates; using Udap.Identity.Provider; using Udap.Server.Configuration; using Udap.Server.DbContexts; diff --git a/examples/Udap.Identity.Provider/Pages/Shared/_Nav.cshtml b/examples/Udap.Identity.Provider/Pages/Shared/_Nav.cshtml index b2539c5c..2e198ddb 100644 --- a/examples/Udap.Identity.Provider/Pages/Shared/_Nav.cshtml +++ b/examples/Udap.Identity.Provider/Pages/Shared/_Nav.cshtml @@ -11,10 +11,10 @@ diff --git a/examples/Udap.Identity.Provider/Program.cs b/examples/Udap.Identity.Provider/Program.cs index 50cc8542..48215329 100644 --- a/examples/Udap.Identity.Provider/Program.cs +++ b/examples/Udap.Identity.Provider/Program.cs @@ -38,6 +38,8 @@ } finally { - Log.Information("Shut down complete"); + Log.Information("Shut down complete"); + Log.CloseAndFlush(); + } \ No newline at end of file diff --git a/examples/Udap.Identity.Provider/Udap.Identity.Provider.csproj b/examples/Udap.Identity.Provider/Udap.Identity.Provider.csproj index cdd5e1c1..e90b8ee8 100644 --- a/examples/Udap.Identity.Provider/Udap.Identity.Provider.csproj +++ b/examples/Udap.Identity.Provider/Udap.Identity.Provider.csproj @@ -23,12 +23,12 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all - - + + @@ -60,6 +60,9 @@ Always + + Always + Always diff --git a/examples/Udap.Identity.Provider/appsettings.Development.json b/examples/Udap.Identity.Provider/appsettings.Development.json index a0979431..b919ac10 100644 --- a/examples/Udap.Identity.Provider/appsettings.Development.json +++ b/examples/Udap.Identity.Provider/appsettings.Development.json @@ -31,6 +31,14 @@ "UdapMetadataConfigs": [ { "Community": "udap://fhirlabs1/", + "SignedMetadataConfig": { + "AuthorizationEndPoint": "https://host.docker.internal:5055/connect/authorize", + "TokenEndpoint": "https://host.docker.internal:5055/connect/token", + "RegistrationEndpoint": "https://host.docker.internal:5055/connect/register" + }, + }, + { + "Community": "udap://Provider2", "SignedMetadataConfig": { "AuthorizationEndPoint": "https://host.docker.internal:5055/connect/authorize", "TokenEndpoint": "https://host.docker.internal:5055/connect/token", @@ -50,6 +58,15 @@ "Password": "udap-test" } ] + }, + { + "Name": "udap://Provider2", + "IssuedCerts": [ + { + "FilePath": "CertStore/issued/fhirLabsApiClientLocalhostCert2.pfx", + "Password": "udap-test" + } + ] } ] } diff --git a/examples/Udap.Identity.Provider/wwwroot/udap.logo.48x48.png b/examples/Udap.Identity.Provider/wwwroot/udap.logo.48x48.png new file mode 100644 index 00000000..52f96f8f Binary files /dev/null and b/examples/Udap.Identity.Provider/wwwroot/udap.logo.48x48.png differ diff --git a/examples/clients/1_UdapClientMetadata/1_UdapClientMetadata.csproj b/examples/clients/1_UdapClientMetadata/1_UdapClientMetadata.csproj index 71da34a2..da26dfa3 100644 --- a/examples/clients/1_UdapClientMetadata/1_UdapClientMetadata.csproj +++ b/examples/clients/1_UdapClientMetadata/1_UdapClientMetadata.csproj @@ -37,7 +37,7 @@ - + diff --git a/examples/clients/2_UdapClientMetadata/2_UdapClientMetadata.csproj b/examples/clients/2_UdapClientMetadata/2_UdapClientMetadata.csproj index 15c7a181..4c8e9a38 100644 --- a/examples/clients/2_UdapClientMetadata/2_UdapClientMetadata.csproj +++ b/examples/clients/2_UdapClientMetadata/2_UdapClientMetadata.csproj @@ -37,7 +37,7 @@ - + diff --git a/examples/clients/UdapEd/Client/Pages/PatientSearch.razor b/examples/clients/UdapEd/Client/Pages/PatientSearch.razor index d6a00220..ca7279aa 100644 --- a/examples/clients/UdapEd/Client/Pages/PatientSearch.razor +++ b/examples/clients/UdapEd/Client/Pages/PatientSearch.razor @@ -81,7 +81,7 @@ -@if (_patients != null || _outComeMessage != null) +@if (_patients != null && _patients.Any() || _outComeMessage != null) { diff --git a/examples/clients/UdapEd/Client/Pages/UdapBusinessToBusiness.razor b/examples/clients/UdapEd/Client/Pages/UdapBusinessToBusiness.razor index 8436c8ac..dc76f15e 100644 --- a/examples/clients/UdapEd/Client/Pages/UdapBusinessToBusiness.razor +++ b/examples/clients/UdapEd/Client/Pages/UdapBusinessToBusiness.razor @@ -85,6 +85,7 @@ Client Token Request +
@TokenRequest1
@TokenRequest2
@@ -254,7 +255,7 @@ Build Access Token Request + OnClick="BuildAccessTokenRequest">Build Access Token Request >> SearchPatient(PatientSearchMod } var bundle = new FhirJsonParser().Parse(result); + var operationOutcome = bundle.Entry.Select(e => e.Resource as OperationOutcome).ToList(); + + if (operationOutcome.Any(o => o != null)) + { + return new FhirResultModel>(operationOutcome.First(), response.StatusCode, response.Version); + } + var patients = bundle.Entry.Select(e => e.Resource as Patient).ToList(); return new FhirResultModel>(patients, response.StatusCode, response.Version); diff --git a/examples/clients/UdapEd/Client/Shared/ClientCertLoader.razor b/examples/clients/UdapEd/Client/Shared/ClientCertLoader.razor index c03c46ac..0cf8fc1e 100644 --- a/examples/clients/UdapEd/Client/Shared/ClientCertLoader.razor +++ b/examples/clients/UdapEd/Client/Shared/ClientCertLoader.razor @@ -38,6 +38,13 @@ Thumbprint (sha1) @AppState.ClientCertificateInfo?.Thumbprint + + @if (AppState.ClientCertificateInfo.CertLoaded == CertLoadedEnum.Expired) + { + + Certificate Expired + + } } @@ -97,6 +104,10 @@ CertLoadedColor = Color.Warning; await AppState.SetPropertyAsync(this, nameof(AppState.CertificateLoaded), false); break; + case CertLoadedEnum.Expired: + CertLoadedColor = Color.Error; + await AppState.SetPropertyAsync(this, nameof(AppState.CertificateLoaded), false); + break; default: CertLoadedColor = Color.Error; await AppState.SetPropertyAsync(this, nameof(AppState.CertificateLoaded), false); diff --git a/examples/clients/UdapEd/Client/UdapEd.Client.csproj b/examples/clients/UdapEd/Client/UdapEd.Client.csproj index ad137a68..bf48ccc1 100644 --- a/examples/clients/UdapEd/Client/UdapEd.Client.csproj +++ b/examples/clients/UdapEd/Client/UdapEd.Client.csproj @@ -18,9 +18,9 @@ - - - + + + diff --git a/examples/clients/UdapEd/Directory.Packages.props b/examples/clients/UdapEd/Directory.Packages.props index 2f9b585f..e6e3c8d9 100644 --- a/examples/clients/UdapEd/Directory.Packages.props +++ b/examples/clients/UdapEd/Directory.Packages.props @@ -11,7 +11,7 @@ udap.logo.48x48.jpg UDAP;FHIR;HL7;X509; UDAP Learning tool is a part of the UDAP reference implementation for .NET. - 0.2.16.3 + 0.2.18.2 $(TAG_NAME_ENV) 0.2.* diff --git a/examples/clients/UdapEd/Server/Controllers/RegisterController.cs b/examples/clients/UdapEd/Server/Controllers/RegisterController.cs index 7ea56a01..d7c01817 100644 --- a/examples/clients/UdapEd/Server/Controllers/RegisterController.cs +++ b/examples/clients/UdapEd/Server/Controllers/RegisterController.cs @@ -7,15 +7,14 @@ // */ #endregion -using System.Net; using System.Net.Http.Headers; +using System.Security.Cryptography.X509Certificates; +using System.Text.Json; +using System.Text.Json.Serialization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.RateLimiting; using Microsoft.IdentityModel.JsonWebTokens; using Microsoft.IdentityModel.Tokens; -using System.Security.Cryptography.X509Certificates; -using System.Text.Json; -using System.Text.Json.Serialization; using Udap.Model; using Udap.Model.Registration; using Udap.Model.Statement; @@ -107,6 +106,12 @@ public IActionResult ValidateCertificate([FromBody] string password) result.DistinguishedName = certificate.SubjectName.Name; result.Thumbprint = certificate.Thumbprint; result.CertLoaded = CertLoadedEnum.Positive; + + if (certificate.NotAfter < DateTime.Now.Date) + { + result.CertLoaded = CertLoadedEnum.Expired; + } + result.SubjectAltNames = certificate .GetSubjectAltNames(n => n.TagNo == (int)X509Extensions.GeneralNameType.URI) .Select(tuple => tuple.Item2) @@ -153,6 +158,11 @@ public IActionResult IsClientCertificateLoaded() result.Thumbprint = clientCert.Thumbprint; result.CertLoaded = CertLoadedEnum.Positive; + if (clientCert.NotAfter < DateTime.Now.Date) + { + result.CertLoaded = CertLoadedEnum.Expired; + } + result.SubjectAltNames = clientCert .GetSubjectAltNames(n => n.TagNo == (int)X509Extensions.GeneralNameType.URI) .Select(tuple => tuple.Item2) @@ -201,13 +211,12 @@ public IActionResult BuildSoftwareStatementWithHeaderForClientCredentials( var document = dcrBuilder - //TODO: this only gets the first SubAltName .WithAudience(request.Audience) .WithExpiration(request.Expiration) .WithJwtId(request.JwtId) .WithClientName(request.ClientName ?? UdapEdConstants.CLIENT_NAME) .WithContacts(request.Contacts) - .WithTokenEndpointAuthMethod(UdapConstants.RegistrationDocumentValues.TokenEndpointAuthMethodValue) + .WithTokenEndpointAuthMethod(RegistrationDocumentValues.TokenEndpointAuthMethodValue) .WithScope(request.Scope ?? string.Empty) .Build(); @@ -276,11 +285,11 @@ public IActionResult BuildSoftwareStatementWithHeaderForAuthorizationCode( .WithJwtId(request.JwtId) .WithClientName(request.ClientName ?? UdapEdConstants.CLIENT_NAME) .WithContacts(request.Contacts) - .WithTokenEndpointAuthMethod(UdapConstants.RegistrationDocumentValues.TokenEndpointAuthMethodValue) + .WithTokenEndpointAuthMethod(RegistrationDocumentValues.TokenEndpointAuthMethodValue) .WithScope(request.Scope ?? string.Empty) .WithResponseTypes(request.ResponseTypes) .WithRedirectUrls(request.RedirectUris) - .WithLogoUri(request.LogoUri ?? "https://udaped.fhirlabs.net/images/hl7/icon-fhir-32.png")//TODO Logo required + .WithLogoUri(request.LogoUri ?? "https://udaped.fhirlabs.net/images/hl7/icon-fhir-32.png") .Build(); var signedSoftwareStatement = @@ -342,14 +351,13 @@ public IActionResult BuildRequestBodyForClientCredentials( dcrBuilder.Document.Issuer = document.Issuer; dcrBuilder.Document.Subject = document.Subject; - //TODO: this only gets the first SubAltName dcrBuilder.WithAudience(document.Audience) .WithExpiration(document.Expiration) .WithJwtId(document.JwtId) .WithClientName(document.ClientName!) .WithContacts(document.Contacts) - .WithTokenEndpointAuthMethod(UdapConstants.RegistrationDocumentValues.TokenEndpointAuthMethodValue) - .WithScope(document.Scope!) ; + .WithTokenEndpointAuthMethod(RegistrationDocumentValues.TokenEndpointAuthMethodValue) + .WithScope(document.Scope) ; if (!request.SoftwareStatement.Contains(RegistrationDocumentValues.GrantTypes)) { @@ -408,11 +416,11 @@ public IActionResult BuildRequestBodyForAuthorizationCode( .WithJwtId(document.JwtId) .WithClientName(document.ClientName!) .WithContacts(document.Contacts) - .WithTokenEndpointAuthMethod(UdapConstants.RegistrationDocumentValues.TokenEndpointAuthMethodValue) + .WithTokenEndpointAuthMethod(RegistrationDocumentValues.TokenEndpointAuthMethodValue) .WithScope(document.Scope!) .WithResponseTypes(document.ResponseTypes) .WithRedirectUrls(document.RedirectUris) - .WithLogoUri(document.LogoUri); //TODO Logo required + .WithLogoUri(document.LogoUri!); @@ -442,8 +450,10 @@ public async Task Register([FromBody] RegistrationRequest request }), new MediaTypeHeaderValue("application/json")); + //TODO: Centralize all registration in UdapClient. See RegisterTieredClient var response = await _httpClient.PostAsync(request.RegistrationEndpoint, content); + if (!response.IsSuccessStatusCode) { var failResult = new ResultModel( @@ -473,7 +483,5 @@ await response.Content.ReadAsStringAsync(), return BadRequest(ex); } - - return NotFound(); } } \ No newline at end of file diff --git a/examples/clients/UdapEd/Server/Dockerfile b/examples/clients/UdapEd/Server/Dockerfile index 01cc14c8..c912a476 100644 --- a/examples/clients/UdapEd/Server/Dockerfile +++ b/examples/clients/UdapEd/Server/Dockerfile @@ -1,6 +1,7 @@ #See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS base +ARG TAG_NAME WORKDIR /app EXPOSE 8080 EXPOSE 443 @@ -16,7 +17,7 @@ WORKDIR /src ENV GCPDeploy=true #this technique of setting env to control version is not working yet. -ARG TAG_NAME=0.2.16.3 +ARG TAG_NAME ENV TAG_NAME_ENV=$TAG_NAME COPY ["nuget.config", "."] @@ -33,7 +34,7 @@ RUN dotnet build "Server/UdapEd.Server.csproj" -c Release -o /app/build FROM build AS publish -ARG TAG_NAME=0.2.16.3 +ARG TAG_NAME RUN dotnet publish "Server/UdapEd.Server.csproj" --version-suffix 99 -c Release -o /app/publish /p:UseAppHost=false FROM base AS final diff --git a/examples/clients/UdapEd/Server/UdapEd.Server.csproj b/examples/clients/UdapEd/Server/UdapEd.Server.csproj index 8b021d83..64dc9384 100644 --- a/examples/clients/UdapEd/Server/UdapEd.Server.csproj +++ b/examples/clients/UdapEd/Server/UdapEd.Server.csproj @@ -21,7 +21,7 @@ - + diff --git a/examples/clients/UdapEd/Shared/Model/CertLoadedEnum.cs b/examples/clients/UdapEd/Shared/Model/CertLoadedEnum.cs index 6fedb1fe..680f38f6 100644 --- a/examples/clients/UdapEd/Shared/Model/CertLoadedEnum.cs +++ b/examples/clients/UdapEd/Shared/Model/CertLoadedEnum.cs @@ -9,4 +9,4 @@ namespace UdapEd.Shared.Model; -public enum CertLoadedEnum { Negative, Positive, InvalidPassword } \ No newline at end of file +public enum CertLoadedEnum { Negative, Positive, InvalidPassword, Expired } \ No newline at end of file diff --git a/examples/clients/UdapEd/Shared/Model/ClientRegistrations.cs b/examples/clients/UdapEd/Shared/Model/ClientRegistrations.cs index bb825056..df4aba9d 100644 --- a/examples/clients/UdapEd/Shared/Model/ClientRegistrations.cs +++ b/examples/clients/UdapEd/Shared/Model/ClientRegistrations.cs @@ -21,23 +21,23 @@ public class ClientRegistrations public Dictionary Registrations { get; set; } = new(); - public ClientRegistration? SetRegistration(string clientId, UdapDynamicClientRegistrationDocument? resultModelResult, Oauth2FlowEnum oauth2Flow, string resourceServer) + public ClientRegistration? SetRegistration(RegistrationDocument registrationDocument, UdapDynamicClientRegistrationDocument? resultModelResult, Oauth2FlowEnum oauth2Flow, string resourceServer) { if (resultModelResult is { Issuer: not null, Audience: not null }) { _clientRegistration = new ClientRegistration { - ClientId = clientId, + ClientId = registrationDocument.ClientId, GrantType = resultModelResult.GrantTypes?.FirstOrDefault(), SubjAltName = resultModelResult.Issuer, UserFlowSelected = oauth2Flow.ToString(), AuthServer = resultModelResult.Audience, ResourceServer = resourceServer, RedirectUri = resultModelResult.RedirectUris, - Scope = resultModelResult.Scope + Scope = registrationDocument.Scope }; - Registrations[clientId] = _clientRegistration; + Registrations[registrationDocument.ClientId] = _clientRegistration; CleanUpRegistration(_clientRegistration); return _clientRegistration; diff --git a/examples/clients/UdapEd/Shared/Model/Registration/UdapDcrBuilderForAuthorizationCodeUnchecked.cs b/examples/clients/UdapEd/Shared/Model/Registration/UdapDcrBuilderForAuthorizationCodeUnchecked.cs index d02ec3b3..18b09322 100644 --- a/examples/clients/UdapEd/Shared/Model/Registration/UdapDcrBuilderForAuthorizationCodeUnchecked.cs +++ b/examples/clients/UdapEd/Shared/Model/Registration/UdapDcrBuilderForAuthorizationCodeUnchecked.cs @@ -24,50 +24,31 @@ public class UdapDcrBuilderForAuthorizationCodeUnchecked : UdapDcrBuilderForAuth set => base.Document = value; } - /// protected UdapDcrBuilderForAuthorizationCodeUnchecked(X509Certificate2 certificate, bool cancelRegistration) : base(cancelRegistration) { this.WithCertificate(certificate); } - /// protected UdapDcrBuilderForAuthorizationCodeUnchecked(bool cancelRegistration) : base(cancelRegistration) { } - /// public new static UdapDcrBuilderForAuthorizationCodeUnchecked Create(X509Certificate2 cert) { return new UdapDcrBuilderForAuthorizationCodeUnchecked(cert, false); } - //TODO: Safe for multi SubjectAltName scenarios - /// - public new static UdapDcrBuilderForAuthorizationCodeUnchecked Create(X509Certificate2 cert, string subjectAltName) - { - return new UdapDcrBuilderForAuthorizationCodeUnchecked(cert, false); - } - - /// + public new static UdapDcrBuilderForAuthorizationCodeUnchecked Create() { return new UdapDcrBuilderForAuthorizationCodeUnchecked(false); } - /// public new static UdapDcrBuilderForAuthorizationCodeUnchecked Cancel(X509Certificate2 cert) { return new UdapDcrBuilderForAuthorizationCodeUnchecked(cert, true); } - - //TODO: Safe for multi SubjectAltName scenarios - /// - public new static UdapDcrBuilderForAuthorizationCodeUnchecked Cancel(X509Certificate2 cert, string subjectAltName) - { - return new UdapDcrBuilderForAuthorizationCodeUnchecked(cert, true); - } - - /// + public new static UdapDcrBuilderForAuthorizationCodeUnchecked Cancel() { return new UdapDcrBuilderForAuthorizationCodeUnchecked(true); diff --git a/examples/clients/UdapEd/Shared/Model/Registration/UdapDcrBuilderForClientCredentialsUnchecked.cs b/examples/clients/UdapEd/Shared/Model/Registration/UdapDcrBuilderForClientCredentialsUnchecked.cs index 6ac31958..ade63f84 100644 --- a/examples/clients/UdapEd/Shared/Model/Registration/UdapDcrBuilderForClientCredentialsUnchecked.cs +++ b/examples/clients/UdapEd/Shared/Model/Registration/UdapDcrBuilderForClientCredentialsUnchecked.cs @@ -33,39 +33,21 @@ protected UdapDcrBuilderForClientCredentialsUnchecked(bool cancelRegistration) : { } - /// public new static UdapDcrBuilderForClientCredentialsUnchecked Create(X509Certificate2 cert) { return new UdapDcrBuilderForClientCredentialsUnchecked(cert, false); } - - //TODO: Safe for multi SubjectAltName scenarios - /// - public new static UdapDcrBuilderForClientCredentialsUnchecked Create(X509Certificate2 cert, string subjectAltName) - { - return new UdapDcrBuilderForClientCredentialsUnchecked(cert, false); - } - - /// + public new static UdapDcrBuilderForClientCredentialsUnchecked Create() { return new UdapDcrBuilderForClientCredentialsUnchecked(false); } - /// public new static UdapDcrBuilderForClientCredentialsUnchecked Cancel(X509Certificate2 cert) { return new UdapDcrBuilderForClientCredentialsUnchecked(cert, true); } - - //TODO: Safe for multi SubjectAltName scenarios - /// - public new static UdapDcrBuilderForClientCredentialsUnchecked Cancel(X509Certificate2 cert, string subjectAltName) - { - return new UdapDcrBuilderForClientCredentialsUnchecked(cert, true); - } - - /// + public new static UdapDcrBuilderForClientCredentialsUnchecked Cancel() { return new UdapDcrBuilderForClientCredentialsUnchecked(true); diff --git a/examples/clients/UdapEd/Shared/UdapEd.Shared.csproj b/examples/clients/UdapEd/Shared/UdapEd.Shared.csproj index aba56ad2..447eaaa5 100644 --- a/examples/clients/UdapEd/Shared/UdapEd.Shared.csproj +++ b/examples/clients/UdapEd/Shared/UdapEd.Shared.csproj @@ -1,4 +1,4 @@ - + net7.0 @@ -29,7 +29,7 @@ - + diff --git a/migrations/UdapDb.SqlServer/Properties/Directory.Packages.props b/migrations/UdapDb.SqlServer/Properties/Directory.Packages.props deleted file mode 100644 index abfbe3f7..00000000 --- a/migrations/UdapDb.SqlServer/Properties/Directory.Packages.props +++ /dev/null @@ -1,33 +0,0 @@ - - - true - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/migrations/UdapDb.SqlServer/SeedData.Auth.Server.cs b/migrations/UdapDb.SqlServer/SeedData.Auth.Server.cs index 2383e4ad..39fc440d 100644 --- a/migrations/UdapDb.SqlServer/SeedData.Auth.Server.cs +++ b/migrations/UdapDb.SqlServer/SeedData.Auth.Server.cs @@ -73,6 +73,7 @@ public static async Task EnsureSeedData(string connectionString, string cer var udapContext = serviceScope.ServiceProvider.GetRequiredService(); await udapContext.Database.MigrateAsync(); + var clientRegistrationStore = serviceScope.ServiceProvider.GetRequiredService(); @@ -486,7 +487,6 @@ private static async Task SeedFhirScopes( { var apiScopes = configDbContext.ApiScopes .Include(s => s.Properties) - .Where(s => s.Enabled) .Select(s => s) .ToList(); @@ -501,6 +501,7 @@ private static async Task SeedFhirScopes( if (apiScope.Name.StartsWith("system/*.")) { apiScope.ShowInDiscoveryDocument = true; + apiScope.Enabled = false; } apiScope.Properties.Add("udap_prefix", "system"); @@ -525,6 +526,7 @@ private static async Task SeedFhirScopes( if (apiScope.Name.StartsWith("patient/*.")) { apiScope.ShowInDiscoveryDocument = true; + apiScope.Enabled = false; } apiScope.Properties.Add("udap_prefix", "user"); @@ -538,7 +540,7 @@ private static async Task SeedFhirScopes( } } - foreach (var scopeName in seedScopes.Where(s => s.StartsWith("patient"))) + foreach (var scopeName in seedScopes.Where(s => s.StartsWith("patient")).ToList()) { if (!apiScopes.Any(s => s.Name == scopeName && s.Properties.Exists(p => p.Key == "udap_prefix" && p.Value == "patient"))) { @@ -548,6 +550,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/migrations/UdapDb.SqlServer/SeedData.Identity.Provider.cs b/migrations/UdapDb.SqlServer/SeedData.Identity.Provider.cs index 2895351e..765649b5 100644 --- a/migrations/UdapDb.SqlServer/SeedData.Identity.Provider.cs +++ b/migrations/UdapDb.SqlServer/SeedData.Identity.Provider.cs @@ -79,18 +79,18 @@ 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://TieredProvider1")) { - var community = new Community { Name = "http://localhost" }; + var community = new Community { Name = "udap://TieredProvider1" }; community.Enabled = true; community.Default = false; udapContext.Communities.Add(community); await udapContext.SaveChangesAsync(); } - if (!udapContext.Communities.Any(c => c.Name == "udap://fhirlabs1/")) + if (!udapContext.Communities.Any(c => c.Name == "udap://Provider2")) { - var community = new Community { Name = "udap://fhirlabs1/" }; + var community = new Community { Name = "udap://Provider2" }; community.Enabled = true; community.Default = true; udapContext.Communities.Add(community); @@ -98,48 +98,39 @@ public static async Task EnsureSeedData(string connectionString, string cer } var assemblyPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); - // - // Anchor surefhirlabs_community + // Anchor localhost_community for Udap.Identity.Provider1 // - var sureFhirLabsAnchor = new X509Certificate2( - Path.Combine(assemblyPath!, certStoreBasePath, "surefhirlabs_community/SureFhirLabs_CA.cer")); + var anchorLocalhostCert = new X509Certificate2( + Path.Combine(assemblyPath!, certStoreBasePath, "localhost_fhirlabs_community1/caLocalhostCert.cer")); - if ((await clientRegistrationStore.GetAnchors("udap://fhirlabs1/")) - .All(a => a.Thumbprint != sureFhirLabsAnchor.Thumbprint)) + if ((await clientRegistrationStore.GetAnchors("udap://TieredProvider1")) + .All(a => a.Thumbprint != anchorLocalhostCert.Thumbprint)) { - var community = udapContext.Communities.Single(c => c.Name == "udap://fhirlabs1/"); - - anchor = new Anchor + var community = udapContext.Communities.Single(c => c.Name == "udap://TieredProvider1"); + var anchor = new Anchor { - BeginDate = sureFhirLabsAnchor.NotBefore.ToUniversalTime(), - EndDate = sureFhirLabsAnchor.NotAfter.ToUniversalTime(), - Name = sureFhirLabsAnchor.Subject, + BeginDate = anchorLocalhostCert.NotBefore.ToUniversalTime(), + EndDate = anchorLocalhostCert.NotAfter.ToUniversalTime(), + Name = anchorLocalhostCert.Subject, Community = community, - X509Certificate = sureFhirLabsAnchor.ToPemFormat(), - Thumbprint = sureFhirLabsAnchor.Thumbprint, + X509Certificate = anchorLocalhostCert.ToPemFormat(), + Thumbprint = anchorLocalhostCert.Thumbprint, Enabled = true }; - udapContext.Anchors.Add(anchor); - await udapContext.SaveChangesAsync(); - } - var intermediateCert = new X509Certificate2( - Path.Combine(assemblyPath!, certStoreBasePath, - "surefhirlabs_community/intermediates/SureFhirLabs_Intermediate.cer")); - - if ((await clientRegistrationStore.GetIntermediateCertificates()) - .All(a => a.Thumbprint != intermediateCert.Thumbprint)) - { - var anchor = udapContext.Anchors.Single(a => a.Thumbprint == sureFhirLabsAnchor.Thumbprint); + await udapContext.SaveChangesAsync(); // // Intermediate surefhirlabs_community // var x509Certificate2Collection = await clientRegistrationStore.GetIntermediateCertificates(); - + + var intermediateCert = new X509Certificate2( + Path.Combine(assemblyPath!, certStoreBasePath, "localhost_fhirlabs_community1/intermediates/intermediateLocalhostCert.cer")); + if (x509Certificate2Collection != null && x509Certificate2Collection.ToList() .All(r => r.Thumbprint != intermediateCert.Thumbprint)) { @@ -161,56 +152,68 @@ public static async Task EnsureSeedData(string connectionString, string cer // - // Anchor localhost_community + // Anchor localhost_fhirlabs_community2 for Udap.Identity.Provider2 // - var anchorLocalhostCert = new X509Certificate2( - Path.Combine(assemblyPath!, certStoreBasePath, "localhost_fhirlabs_community1/caLocalhostCert.cer")); + var anchorUdapIdentityProvider2 = new X509Certificate2( + Path.Combine(assemblyPath!, certStoreBasePath, "localhost_fhirlabs_community2/caLocalhostCert2.cer")); - if ((await clientRegistrationStore.GetAnchors("http://localhost")) - .All(a => a.Thumbprint != anchorLocalhostCert.Thumbprint)) + if ((await clientRegistrationStore.GetAnchors("udap://Provider2")) + .All(a => a.Thumbprint != anchorUdapIdentityProvider2.Thumbprint)) { - var community = udapContext.Communities.Single(c => c.Name == "http://localhost"); - var anchor = new Anchor + var community = udapContext.Communities.Single(c => c.Name == "udap://Provider2"); + + anchor = new Anchor { - BeginDate = anchorLocalhostCert.NotBefore.ToUniversalTime(), - EndDate = anchorLocalhostCert.NotAfter.ToUniversalTime(), - Name = anchorLocalhostCert.Subject, + BeginDate = anchorUdapIdentityProvider2.NotBefore.ToUniversalTime(), + EndDate = anchorUdapIdentityProvider2.NotAfter.ToUniversalTime(), + Name = anchorUdapIdentityProvider2.Subject, Community = community, - X509Certificate = anchorLocalhostCert.ToPemFormat(), - Thumbprint = anchorLocalhostCert.Thumbprint, + X509Certificate = anchorUdapIdentityProvider2.ToPemFormat(), + Thumbprint = anchorUdapIdentityProvider2.Thumbprint, Enabled = true }; - udapContext.Anchors.Add(anchor); + udapContext.Anchors.Add(anchor); await udapContext.SaveChangesAsync(); + } + + var intermediateCertProvider2 = new X509Certificate2( + Path.Combine(assemblyPath!, certStoreBasePath, + "localhost_fhirlabs_community2/intermediates/intermediateLocalhostCert2.cer")); + + if ((await clientRegistrationStore.GetIntermediateCertificates()) + .All(a => a.Thumbprint != intermediateCertProvider2.Thumbprint)) + { + var anchorProvider2 = udapContext.Anchors.Single(a => a.Thumbprint == anchorUdapIdentityProvider2.Thumbprint); // // 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)) + .All(r => r.Thumbprint != intermediateCertProvider2.Thumbprint)) { udapContext.IntermediateCertificates.Add(new Intermediate { - BeginDate = intermediateCert.NotBefore.ToUniversalTime(), - EndDate = intermediateCert.NotAfter.ToUniversalTime(), - Name = intermediateCert.Subject, - X509Certificate = intermediateCert.ToPemFormat(), - Thumbprint = intermediateCert.Thumbprint, + BeginDate = intermediateCertProvider2.NotBefore.ToUniversalTime(), + EndDate = intermediateCertProvider2.NotAfter.ToUniversalTime(), + Name = intermediateCertProvider2.Subject, + X509Certificate = intermediateCertProvider2.ToPemFormat(), + Thumbprint = intermediateCertProvider2.Thumbprint, Enabled = true, - Anchor = anchor + Anchor = anchorProvider2 }); await udapContext.SaveChangesAsync(); } } + + + + /* * "openid", "fhirUser", diff --git a/migrations/UdapDb.SqlServer/Seed_GCP_Auth_Server.cs b/migrations/UdapDb.SqlServer/Seed_GCP_Auth_Server.cs index fedee787..618cb6d0 100644 --- a/migrations/UdapDb.SqlServer/Seed_GCP_Auth_Server.cs +++ b/migrations/UdapDb.SqlServer/Seed_GCP_Auth_Server.cs @@ -1,13 +1,12 @@ -/* - Copyright (c) Joseph Shook. All rights reserved. - Authors: - Joseph Shook Joseph.Shook@Surescripts.com +#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 - See LICENSE in the project root for license information. -*/ - - -using System.Linq; using System.Reflection; using System.Security.Cryptography.X509Certificates; using System.Text; @@ -16,7 +15,6 @@ Joseph Shook Joseph.Shook@Surescripts.com using Duende.IdentityServer.EntityFramework.Mappers; using Duende.IdentityServer.EntityFramework.Storage; using Duende.IdentityServer.Models; -using Hl7.Fhir.Rest; using Microsoft.EntityFrameworkCore; using Serilog; using Udap.Common.Extensions; @@ -28,7 +26,6 @@ Joseph Shook Joseph.Shook@Surescripts.com using Udap.Server.Stores; using Udap.Util.Extensions; using ILogger = Serilog.ILogger; -using Task = System.Threading.Tasks.Task; namespace UdapDb; @@ -76,9 +73,9 @@ public static async Task EnsureSeedData(string connectionString, string cer var udapContext = serviceScope.ServiceProvider.GetRequiredService(); await udapContext.Database.MigrateAsync(); - var clientRegistrationStore = serviceScope.ServiceProvider.GetRequiredService(); - + var clientRegistrationStore = serviceScope.ServiceProvider.GetRequiredService(); + if (!udapContext.Communities.Any(c => c.Name == "http://localhost")) { var community = new Community { Name = "http://localhost" }; @@ -98,9 +95,6 @@ public static async Task EnsureSeedData(string connectionString, string cer } var assemblyPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); - - - // // Anchor surefhirlabs_community // @@ -300,7 +294,6 @@ private static async Task SeedFhirScopes(ConfigurationDbContext configDbContext, { var apiScopes = configDbContext.ApiScopes .Include(s => s.Properties) - .Where(s => s.Enabled) .Select(s => s) .ToList(); @@ -314,6 +307,7 @@ private static async Task SeedFhirScopes(ConfigurationDbContext configDbContext, if (apiScope.Name.StartsWith("system/*.")) { apiScope.ShowInDiscoveryDocument = true; + apiScope.Enabled = false; } apiScope.Properties.Add("udap_prefix", "system"); apiScope.Properties.Add("smart_version", version.ToString()); @@ -331,6 +325,7 @@ private static async Task SeedFhirScopes(ConfigurationDbContext configDbContext, if (apiScope.Name.StartsWith("patient/*.")) { apiScope.ShowInDiscoveryDocument = true; + apiScope.Enabled = false; } apiScope.Properties.Add("udap_prefix", "user"); apiScope.Properties.Add("smart_version", version.ToString()); @@ -347,6 +342,7 @@ private static async Task SeedFhirScopes(ConfigurationDbContext configDbContext, if (apiScope.Name.StartsWith("patient/*.")) { apiScope.ShowInDiscoveryDocument = true; + apiScope.Enabled = false; } apiScope.Properties.Add("udap_prefix", "patient"); apiScope.Properties.Add("smart_version", version.ToString()); diff --git a/migrations/UdapDb.SqlServer/Udap/Server/Migrations/SqlServer/UdapDb/20230826205429_InitialSqlServerUdap.Designer.cs b/migrations/UdapDb.SqlServer/Udap/Server/Migrations/SqlServer/UdapDb/20231019222837_InitialSqlServerUdap.Designer.cs similarity index 99% rename from migrations/UdapDb.SqlServer/Udap/Server/Migrations/SqlServer/UdapDb/20230826205429_InitialSqlServerUdap.Designer.cs rename to migrations/UdapDb.SqlServer/Udap/Server/Migrations/SqlServer/UdapDb/20231019222837_InitialSqlServerUdap.Designer.cs index 06428eee..3b20a0fa 100644 --- a/migrations/UdapDb.SqlServer/Udap/Server/Migrations/SqlServer/UdapDb/20230826205429_InitialSqlServerUdap.Designer.cs +++ b/migrations/UdapDb.SqlServer/Udap/Server/Migrations/SqlServer/UdapDb/20231019222837_InitialSqlServerUdap.Designer.cs @@ -12,7 +12,7 @@ namespace Udap.Server.Migrations.SqlServer.UdapDb { [DbContext(typeof(UdapDbContext))] - [Migration("20230826205429_InitialSqlServerUdap")] + [Migration("20231019222837_InitialSqlServerUdap")] partial class InitialSqlServerUdap { /// @@ -20,7 +20,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "7.0.10") + .HasAnnotation("ProductVersion", "7.0.12") .HasAnnotation("Relational:MaxIdentifierLength", 128); SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); @@ -633,6 +633,10 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("nvarchar(max)"); + b.Property("TokenEndpoint") + .IsRequired() + .HasColumnType("nvarchar(max)"); + b.HasKey("Id"); b.ToTable("TieredClients"); diff --git a/migrations/UdapDb.SqlServer/Udap/Server/Migrations/SqlServer/UdapDb/20230826205429_InitialSqlServerUdap.cs b/migrations/UdapDb.SqlServer/Udap/Server/Migrations/SqlServer/UdapDb/20231019222837_InitialSqlServerUdap.cs similarity index 98% rename from migrations/UdapDb.SqlServer/Udap/Server/Migrations/SqlServer/UdapDb/20230826205429_InitialSqlServerUdap.cs rename to migrations/UdapDb.SqlServer/Udap/Server/Migrations/SqlServer/UdapDb/20231019222837_InitialSqlServerUdap.cs index ff22f119..ef7e0481 100644 --- a/migrations/UdapDb.SqlServer/Udap/Server/Migrations/SqlServer/UdapDb/20230826205429_InitialSqlServerUdap.cs +++ b/migrations/UdapDb.SqlServer/Udap/Server/Migrations/SqlServer/UdapDb/20231019222837_InitialSqlServerUdap.cs @@ -37,7 +37,8 @@ protected override void Up(MigrationBuilder migrationBuilder) RedirectUri = table.Column(type: "nvarchar(max)", nullable: false), ClientUriSan = table.Column(type: "nvarchar(max)", nullable: false), CommunityId = table.Column(type: "int", nullable: false), - Enabled = table.Column(type: "bit", nullable: false) + Enabled = table.Column(type: "bit", nullable: false), + TokenEndpoint = table.Column(type: "nvarchar(max)", nullable: false) }, constraints: table => { diff --git a/migrations/UdapDb.SqlServer/Udap/Server/Migrations/SqlServer/UdapDb/UdapDbContextModelSnapshot.cs b/migrations/UdapDb.SqlServer/Udap/Server/Migrations/SqlServer/UdapDb/UdapDbContextModelSnapshot.cs index 328960fa..be580692 100644 --- a/migrations/UdapDb.SqlServer/Udap/Server/Migrations/SqlServer/UdapDb/UdapDbContextModelSnapshot.cs +++ b/migrations/UdapDb.SqlServer/Udap/Server/Migrations/SqlServer/UdapDb/UdapDbContextModelSnapshot.cs @@ -17,7 +17,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "7.0.10") + .HasAnnotation("ProductVersion", "7.0.12") .HasAnnotation("Relational:MaxIdentifierLength", 128); SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); @@ -630,6 +630,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("nvarchar(max)"); + b.Property("TokenEndpoint") + .IsRequired() + .HasColumnType("nvarchar(max)"); + b.HasKey("Id"); b.ToTable("TieredClients"); diff --git a/migrations/UdapDb.SqlServer/Udap/Server/Migrations/SqlServer/udapSqlServerDb.sql b/migrations/UdapDb.SqlServer/Udap/Server/Migrations/SqlServer/udapSqlServerDb.sql index 0db8f414..434f9fc9 100644 --- a/migrations/UdapDb.SqlServer/Udap/Server/Migrations/SqlServer/udapSqlServerDb.sql +++ b/migrations/UdapDb.SqlServer/Udap/Server/Migrations/SqlServer/udapSqlServerDb.sql @@ -28,6 +28,7 @@ CREATE TABLE [TieredClients] ( [ClientUriSan] nvarchar(max) NOT NULL, [CommunityId] int NOT NULL, [Enabled] bit NOT NULL, + [TokenEndpoint] nvarchar(max) NOT NULL, CONSTRAINT [PK_TieredClients] PRIMARY KEY ([Id]) ); GO @@ -112,7 +113,7 @@ CREATE INDEX [IX_UdapIntermediateCertificates_AnchorId] ON [UdapIntermediateCert GO INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) -VALUES (N'20230826205429_InitialSqlServerUdap', N'7.0.10'); +VALUES (N'20231019222837_InitialSqlServerUdap', N'7.0.12'); GO COMMIT; diff --git a/migrations/UdapDb.SqlServer/UdapDb.SqlServer.csproj b/migrations/UdapDb.SqlServer/UdapDb.SqlServer.csproj index c56ea8a9..6328cdbe 100644 --- a/migrations/UdapDb.SqlServer/UdapDb.SqlServer.csproj +++ b/migrations/UdapDb.SqlServer/UdapDb.SqlServer.csproj @@ -12,12 +12,12 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + +