From 1218b7c9d96d230723ae39832d5730a510f3d508 Mon Sep 17 00:00:00 2001 From: Joseph Shook Date: Tue, 10 Oct 2023 23:30:00 -0700 Subject: [PATCH 01/42] Finishing up a MVP for Scope expansions https://github.com/users/JoeShook/projects/2?pane=issue&itemId=39202303 Expand wild card scope requests to actual scopes in the ApiScopes configuration store. Map scope requests like system/Patient.rs to actual ApiScopes in configuration store. For example system/Patient.r and system.Patient.s. --- .../ServiceCollectionExtensions.cs | 8 +- .../UdapBuilderExtensions/UdapCore.cs | 2 +- ...namicClientRegistrationValidationResult.cs | 1 + .../UdapDynamicClientRegistrationValidator.cs | 27 ++- .../Default/DefaultScopeExpander.cs | 16 +- .../Validation/HL7SmartScopeExpander.cs | 205 ++++++++++++++++++ Udap.Server/Validation/IScopeExpander.cs | 12 +- Udap.Server/Validation/SmartV2Expander.cs | 138 ------------ Udap.sln | 6 + .../Common/UdapAuthServerPipeline.cs | 8 +- .../Basic/ClientCredentialsUdapModeTests.cs | 12 +- .../RegistrationAndChangeRegistrationTests.cs | 4 +- .../Conformance/Basic/ScopeExpansionTests.cs | 161 +++++++++++++- .../Conformance/Tiered/TieredOauthTests.cs | 3 +- .../IntegrationRegistrationTests.cs | 6 +- .../Validators/UdapDCRValidatorTests.cs | 5 +- .../Udap.Auth.Server/HostingExtensions.cs | 3 +- .../Client/Pages/UdapBusinessToBusiness.razor | 3 +- .../Client/Pages/UdapRegistration.razor.cs | 2 +- .../Shared/Model/ClientRegistrations.cs | 8 +- .../UdapDb.SqlServer/SeedData.Auth.Server.cs | 3 + .../UdapDb.SqlServer/Seed_GCP_Auth_Server.cs | 3 + 22 files changed, 460 insertions(+), 176 deletions(-) create mode 100644 Udap.Server/Validation/HL7SmartScopeExpander.cs delete mode 100644 Udap.Server/Validation/SmartV2Expander.cs 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.Server/Configuration/DependencyInjection/UdapBuilderExtensions/UdapCore.cs b/Udap.Server/Configuration/DependencyInjection/UdapBuilderExtensions/UdapCore.cs index 1a88fa1f..47eb7154 100644 --- a/Udap.Server/Configuration/DependencyInjection/UdapBuilderExtensions/UdapCore.cs +++ b/Udap.Server/Configuration/DependencyInjection/UdapBuilderExtensions/UdapCore.cs @@ -95,7 +95,7 @@ public static IUdapServiceBuilder AddUdapConfigurationStore( public static IUdapServiceBuilder AddSmartV2Expander(this IUdapServiceBuilder builder) { - builder.Services.AddScoped(); + builder.Services.AddScoped(); return builder; } 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..2fdbbf1a 100644 --- a/Udap.Server/Registration/UdapDynamicClientRegistrationValidator.cs +++ b/Udap.Server/Registration/UdapDynamicClientRegistrationValidator.cs @@ -16,10 +16,12 @@ // using System.IdentityModel.Tokens.Jwt; +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; @@ -49,6 +51,7 @@ public class UdapDynamicClientRegistrationValidator : IUdapDynamicClientRegistra private readonly ServerSettings _serverSettings; private readonly IHttpContextAccessor _httpContextAccessor; private readonly IScopeExpander _scopeExpander; + private readonly IResourceStore _resourceStore; private const string Purpose = nameof(UdapDynamicClientRegistrationValidator); @@ -58,6 +61,7 @@ public UdapDynamicClientRegistrationValidator( ServerSettings serverSettings, IHttpContextAccessor httpContextAccessor, IScopeExpander scopeExpander, + IResourceStore resourceStore, //TODO use CachingResourceStore ILogger logger) { _trustChainValidator = trustChainValidator; @@ -65,6 +69,7 @@ public UdapDynamicClientRegistrationValidator( _serverSettings = serverSettings; _httpContextAccessor = httpContextAccessor; _scopeExpander = scopeExpander; + _resourceStore = resourceStore; _logger = logger; } @@ -481,13 +486,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(); + } } 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/Udap.sln b/Udap.sln index 901baa15..4564a12c 100644 --- a/Udap.sln +++ b/Udap.sln @@ -92,6 +92,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Udap.Auth.Server.Admin", "e EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Udap.Auth.Server", "examples\Udap.Auth.Server\Udap.Auth.Server.csproj", "{FD9FBB5C-DBCF-4C1D-9E21-A98485D179E6}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duende.IdentityServer", "..\..\..\DuendeSoftware\IdentityServer\src\IdentityServer\Duende.IdentityServer.csproj", "{F3308A5C-5BBA-4BF6-9F5A-85818D19B55F}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -206,6 +208,10 @@ Global {FD9FBB5C-DBCF-4C1D-9E21-A98485D179E6}.Debug|Any CPU.Build.0 = Debug|Any CPU {FD9FBB5C-DBCF-4C1D-9E21-A98485D179E6}.Release|Any CPU.ActiveCfg = Release|Any CPU {FD9FBB5C-DBCF-4C1D-9E21-A98485D179E6}.Release|Any CPU.Build.0 = Release|Any CPU + {F3308A5C-5BBA-4BF6-9F5A-85818D19B55F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F3308A5C-5BBA-4BF6-9F5A-85818D19B55F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F3308A5C-5BBA-4BF6-9F5A-85818D19B55F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F3308A5C-5BBA-4BF6-9F5A-85818D19B55F}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/_tests/UdapServer.Tests/Common/UdapAuthServerPipeline.cs b/_tests/UdapServer.Tests/Common/UdapAuthServerPipeline.cs index f28452ca..5054bc0f 100644 --- a/_tests/UdapServer.Tests/Common/UdapAuthServerPipeline.cs +++ b/_tests/UdapServer.Tests/Common/UdapAuthServerPipeline.cs @@ -164,14 +164,14 @@ public void ConfigureServices(WebHostBuilderContext builder, IServiceCollection services.AddSingleton(sp => new TrustAnchorFileStore( sp.GetRequiredService>(), - new Mock>().Object)); - + new Mock>().Object)); + services.AddUdapServer(BaseUrl, "FhirLabsApi") .AddUdapInMemoryApiScopes(ApiScopes) .AddInMemoryUdapCertificates(Communities) - .AddUdapResponseGenerators() - .AddSmartV2Expander(); + .AddUdapResponseGenerators(); + //.AddSmartV2Expander(); services.AddIdentityServer(options => diff --git a/_tests/UdapServer.Tests/Conformance/Basic/ClientCredentialsUdapModeTests.cs b/_tests/UdapServer.Tests/Conformance/Basic/ClientCredentialsUdapModeTests.cs index 5f4e9a96..6c0ed2bf 100644 --- a/_tests/UdapServer.Tests/Conformance/Basic/ClientCredentialsUdapModeTests.cs +++ b/_tests/UdapServer.Tests/Conformance/Basic/ClientCredentialsUdapModeTests.cs @@ -145,8 +145,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] @@ -406,7 +406,7 @@ public async Task UpdateRegistration() regResponse.StatusCode.Should().Be(HttpStatusCode.OK); 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(clientIdWithDefaultSubAltName); @@ -449,7 +449,7 @@ public async Task UpdateRegistration() 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); @@ -636,7 +636,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); @@ -956,7 +956,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..0bed08b5 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")); } @@ -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..5d22b580 100644 --- a/_tests/UdapServer.Tests/Conformance/Basic/ScopeExpansionTests.cs +++ b/_tests/UdapServer.Tests/Conformance/Basic/ScopeExpansionTests.cs @@ -36,6 +36,7 @@ using Udap.Client.Configuration; using Udap.Common.Extensions; using Udap.Server.Validation; +using System.IdentityModel.Tokens.Jwt; namespace UdapServer.Tests.Conformance.Basic; @@ -67,7 +68,8 @@ public ScopeExpansionTests(ITestOutputHelper testOutputHelper) ClientName = "Mock Client", Contacts = new HashSet { "mailto:Joseph.Shook@Surescripts.com", "mailto:JoeShook@gmail.com" } }); - + + s.AddScoped(); }; _mockPipeline.OnPreConfigureServices += (_, s) => @@ -112,9 +114,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()); @@ -144,7 +150,7 @@ public void GenerateCombinations_ReturnsUniqueStringCombinationsInGivenOrder(str } } - + [Fact] public async Task ScopeV2WithClientCredentialsTest() { @@ -277,6 +283,7 @@ public async Task ScopeV2WithClientCredentialsTest() tokenResponse.Scope.Should().Be("system/Patient.rs", tokenResponse.Raw); + // // Again wild card expansion: TODO // @@ -361,6 +368,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 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() + ); + + 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 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() + ); + + 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 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() + ); + + 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() { diff --git a/_tests/UdapServer.Tests/Conformance/Tiered/TieredOauthTests.cs b/_tests/UdapServer.Tests/Conformance/Tiered/TieredOauthTests.cs index 0877b919..984b715e 100644 --- a/_tests/UdapServer.Tests/Conformance/Tiered/TieredOauthTests.cs +++ b/_tests/UdapServer.Tests/Conformance/Tiered/TieredOauthTests.cs @@ -173,8 +173,7 @@ private void BuildUdapAuthorizationServer(X509Certificate2 sureFhirLabsAnchor, X // _mockAuthorServerPipeline. - - + _mockAuthorServerPipeline.IdentityScopes.Add(new IdentityResources.OpenId()); _mockAuthorServerPipeline.IdentityScopes.Add(new IdentityResources.Profile()); _mockAuthorServerPipeline.IdentityScopes.Add(new UdapIdentityResources.Udap()); 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/Validators/UdapDCRValidatorTests.cs b/_tests/UdapServer.Tests/Validators/UdapDCRValidatorTests.cs index e712f34e..d7f8e10e 100644 --- a/_tests/UdapServer.Tests/Validators/UdapDCRValidatorTests.cs +++ b/_tests/UdapServer.Tests/Validators/UdapDCRValidatorTests.cs @@ -1,4 +1,5 @@ -using FluentAssertions; +using Duende.IdentityServer.Stores; +using FluentAssertions; using IdentityModel; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; @@ -116,6 +117,7 @@ public async Task ValidateJti_And_ReplayTest() serverSettings, mockHttpContextAccessor.Object, new DefaultScopeExpander(), + new Mock().Object, new Mock>().Object); @@ -196,6 +198,7 @@ private static UdapDynamicClientRegistrationDocument BuildUdapDcrValidator( serverSettings, mockHttpContextAccessor.Object, new DefaultScopeExpander(), + new Mock().Object, new Mock>().Object); return document; } diff --git a/examples/Udap.Auth.Server/HostingExtensions.cs b/examples/Udap.Auth.Server/HostingExtensions.cs index 6f1a11bc..ece38920 100644 --- a/examples/Udap.Auth.Server/HostingExtensions.cs +++ b/examples/Udap.Auth.Server/HostingExtensions.cs @@ -91,7 +91,8 @@ public static WebApplication ConfigureServices(this WebApplicationBuilder builde _ => throw new Exception($"Unsupported provider: {provider}") }) - .AddUdapResponseGenerators(); + .AddUdapResponseGenerators() + .AddSmartV2Expander(); builder.Services.Configure(builder.Configuration.GetSection(Common.Constants.UDAP_FILE_STORE_MANIFEST)); 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 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/migrations/UdapDb.SqlServer/SeedData.Auth.Server.cs b/migrations/UdapDb.SqlServer/SeedData.Auth.Server.cs index 2383e4ad..15550856 100644 --- a/migrations/UdapDb.SqlServer/SeedData.Auth.Server.cs +++ b/migrations/UdapDb.SqlServer/SeedData.Auth.Server.cs @@ -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"); @@ -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/Seed_GCP_Auth_Server.cs b/migrations/UdapDb.SqlServer/Seed_GCP_Auth_Server.cs index fedee787..a555a671 100644 --- a/migrations/UdapDb.SqlServer/Seed_GCP_Auth_Server.cs +++ b/migrations/UdapDb.SqlServer/Seed_GCP_Auth_Server.cs @@ -314,6 +314,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 +332,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 +349,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()); From 508311bee0b60b6d4b59b7e73424960bc8068b48 Mon Sep 17 00:00:00 2001 From: Joseph Shook Date: Tue, 10 Oct 2023 23:33:05 -0700 Subject: [PATCH 02/42] Update Udap.sln --- Udap.sln | 6 ------ 1 file changed, 6 deletions(-) diff --git a/Udap.sln b/Udap.sln index 4564a12c..901baa15 100644 --- a/Udap.sln +++ b/Udap.sln @@ -92,8 +92,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Udap.Auth.Server.Admin", "e EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Udap.Auth.Server", "examples\Udap.Auth.Server\Udap.Auth.Server.csproj", "{FD9FBB5C-DBCF-4C1D-9E21-A98485D179E6}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duende.IdentityServer", "..\..\..\DuendeSoftware\IdentityServer\src\IdentityServer\Duende.IdentityServer.csproj", "{F3308A5C-5BBA-4BF6-9F5A-85818D19B55F}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -208,10 +206,6 @@ Global {FD9FBB5C-DBCF-4C1D-9E21-A98485D179E6}.Debug|Any CPU.Build.0 = Debug|Any CPU {FD9FBB5C-DBCF-4C1D-9E21-A98485D179E6}.Release|Any CPU.ActiveCfg = Release|Any CPU {FD9FBB5C-DBCF-4C1D-9E21-A98485D179E6}.Release|Any CPU.Build.0 = Release|Any CPU - {F3308A5C-5BBA-4BF6-9F5A-85818D19B55F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F3308A5C-5BBA-4BF6-9F5A-85818D19B55F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F3308A5C-5BBA-4BF6-9F5A-85818D19B55F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F3308A5C-5BBA-4BF6-9F5A-85818D19B55F}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 49c344ccd34a0d7ed5764acccf15190f964f3354 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 11 Oct 2023 12:49:34 +0000 Subject: [PATCH 03/42] Bump Microsoft.AspNetCore.Mvc.Testing from 7.0.11 to 7.0.12 Bumps [Microsoft.AspNetCore.Mvc.Testing](https://github.com/dotnet/aspnetcore) from 7.0.11 to 7.0.12. - [Release notes](https://github.com/dotnet/aspnetcore/releases) - [Changelog](https://github.com/dotnet/aspnetcore/blob/main/docs/ReleasePlanning.md) - [Commits](https://github.com/dotnet/aspnetcore/compare/v7.0.11...v7.0.12) --- updated-dependencies: - dependency-name: Microsoft.AspNetCore.Mvc.Testing dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- _tests/Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_tests/Directory.Packages.props b/_tests/Directory.Packages.props index dc983d1d..867bd21c 100644 --- a/_tests/Directory.Packages.props +++ b/_tests/Directory.Packages.props @@ -12,7 +12,7 @@ - + From c1fca798ad32fad50874691227ae62885d26b114 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 11 Oct 2023 12:50:07 +0000 Subject: [PATCH 04/42] Bump dotnet-ef from 7.0.11 to 7.0.12 Bumps [dotnet-ef](https://github.com/dotnet/efcore) from 7.0.11 to 7.0.12. - [Release notes](https://github.com/dotnet/efcore/releases) - [Commits](https://github.com/dotnet/efcore/compare/v7.0.11...v7.0.12) --- updated-dependencies: - dependency-name: dotnet-ef dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .config/dotnet-tools.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 96be437d..2f789b03 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.12", "commands": [ "dotnet-ef" ] From c64a793b6fd56edd3824b993b7ea252b43cb5d38 Mon Sep 17 00:00:00 2001 From: Joseph Shook Date: Wed, 11 Oct 2023 11:44:10 -0700 Subject: [PATCH 05/42] Installing and deploying GCFuse bug work around Google cloud is having issues with previous guidance pulling in gcsfuse: https://github.com/GoogleCloudPlatform/gcsfuse/issues/1424 --- examples/FhirLabsApi/Dockerfile.gcp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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 From ccf4da3684a0b77ad9bd7accadc2c1811a5dec59 Mon Sep 17 00:00:00 2001 From: Joseph Shook Date: Thu, 12 Oct 2023 06:04:12 -0700 Subject: [PATCH 06/42] Adding Test specific appsettings. For some reason my environment used to pickup the appsettings.json file for TieredOAuthTests. Specifically the UdapIdentityServerPipeline was picking up appsettings.json from another tests dependency on a Auth.Server, via WebApplicationFactory. In that file there is not UdapMetadaOptions so it was effectivly disabled in the test. I kind of expected tha tthis would have failed all the time historicaly. --- _tests/UdapServer.Tests/Common/UdapAuthServerPipeline.cs | 2 +- _tests/UdapServer.Tests/Common/UdapIdentityServerPipeline.cs | 2 +- _tests/UdapServer.Tests/UdapServer.Tests.csproj | 4 ++-- .../{appsettings.json => appsettings.Test.json} | 0 4 files changed, 4 insertions(+), 4 deletions(-) rename _tests/UdapServer.Tests/{appsettings.json => appsettings.Test.json} (100%) diff --git a/_tests/UdapServer.Tests/Common/UdapAuthServerPipeline.cs b/_tests/UdapServer.Tests/Common/UdapAuthServerPipeline.cs index 5054bc0f..63b93188 100644 --- a/_tests/UdapServer.Tests/Common/UdapAuthServerPipeline.cs +++ b/_tests/UdapServer.Tests/Common/UdapAuthServerPipeline.cs @@ -120,7 +120,7 @@ public void Initialize(string basePath = null, bool enableLogging = false) } }); - builder.ConfigureAppConfiguration(configure => configure.AddJsonFile("appsettings.json")); + builder.ConfigureAppConfiguration(configure => configure.AddJsonFile("appsettings.Test.json")); if (enableLogging) { diff --git a/_tests/UdapServer.Tests/Common/UdapIdentityServerPipeline.cs b/_tests/UdapServer.Tests/Common/UdapIdentityServerPipeline.cs index ffbd7328..529fd8cc 100644 --- a/_tests/UdapServer.Tests/Common/UdapIdentityServerPipeline.cs +++ b/_tests/UdapServer.Tests/Common/UdapIdentityServerPipeline.cs @@ -116,7 +116,7 @@ public void Initialize(string basePath = null, bool enableLogging = false) } }); - builder.ConfigureAppConfiguration(configure => configure.AddJsonFile("appsettings.json")); + builder.ConfigureAppConfiguration(configure => configure.AddJsonFile("appsettings.Test.json")); if (enableLogging) { diff --git a/_tests/UdapServer.Tests/UdapServer.Tests.csproj b/_tests/UdapServer.Tests/UdapServer.Tests.csproj index c399fd72..df1835bc 100644 --- a/_tests/UdapServer.Tests/UdapServer.Tests.csproj +++ b/_tests/UdapServer.Tests/UdapServer.Tests.csproj @@ -16,11 +16,11 @@ - + - + PreserveNewest true PreserveNewest diff --git a/_tests/UdapServer.Tests/appsettings.json b/_tests/UdapServer.Tests/appsettings.Test.json similarity index 100% rename from _tests/UdapServer.Tests/appsettings.json rename to _tests/UdapServer.Tests/appsettings.Test.json From 29387382b68a174cb2d18d6fa04ce03a0703389a Mon Sep 17 00:00:00 2001 From: Joseph Shook Date: Thu, 12 Oct 2023 16:01:30 -0700 Subject: [PATCH 07/42] Working in progress retained for Dynamic Provider support for Tiered OAuth. --- .../UdapInMemoryIdentityProviderStore.cs | 45 ++ .../TieredOAuthAuthenticationHandler.cs | 28 +- _tests/Udap.PKI.Generator/BuildTestCerts.cs | 34 +- .../UdapServer.Tests/Common/TestExtensions.cs | 50 +- .../Common/UdapAuthServerPipeline.cs | 14 +- .../Common/UdapIdentityServerPipeline.cs | 129 +++-- .../Conformance/Tiered/TieredOauthTests.cs | 443 +++++++++++++++++- .../UdapServer.Tests/UdapServer.Tests.csproj | 25 +- _tests/UdapServer.Tests/appsettings.Auth.json | 47 ++ ...ttings.Test.json => appsettings.Idp1.json} | 2 - _tests/UdapServer.Tests/appsettings.Idp2.json | 50 ++ 11 files changed, 783 insertions(+), 84 deletions(-) create mode 100644 Udap.Server/Hosting/DynamicProviders/Store/UdapInMemoryIdentityProviderStore.cs create mode 100644 _tests/UdapServer.Tests/appsettings.Auth.json rename _tests/UdapServer.Tests/{appsettings.Test.json => appsettings.Idp1.json} (95%) create mode 100644 _tests/UdapServer.Tests/appsettings.Idp2.json diff --git a/Udap.Server/Hosting/DynamicProviders/Store/UdapInMemoryIdentityProviderStore.cs b/Udap.Server/Hosting/DynamicProviders/Store/UdapInMemoryIdentityProviderStore.cs new file mode 100644 index 00000000..63585186 --- /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("InMemoryOidcProviderStore.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("InMemoryOidcProviderStore.GetByScheme"); + + var item = _providers.FirstOrDefault(x => x.Scheme == scheme); + return Task.FromResult(item); + } +} \ No newline at end of file diff --git a/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthAuthenticationHandler.cs b/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthAuthenticationHandler.cs index 518d5561..1b0ee0aa 100644 --- a/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthAuthenticationHandler.cs +++ b/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthAuthenticationHandler.cs @@ -11,6 +11,7 @@ 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; @@ -416,14 +417,27 @@ 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 community = (HttpUtility.ParseQueryString(idpUri.Query).GetValues("community") ?? Array.Empty()).LastOrDefault(); + var idp = idpUri.OriginalString; + if (community != null) + { + if (idp.Contains($":{{idpUri.Port}}")) + { + idp = $"{idpUri.Scheme}://{idpUri.Host}:{idpUri.Port}{idpUri.LocalPath}"; + } + else + { + idp = $"{idpUri.Scheme}://{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); @@ -506,8 +520,12 @@ protected override async Task HandleChallengeAsync(AuthenticationProperties prop // UdapDcrBuilderForAuthorizationCode or UdapDcrBuilderForClientCredentials var document = await _udapClient.RegisterTieredClient( resourceHolderRedirectUrl, - _certificateStore.IssuedCertificates.Where(ic => ic.IdPBaseUrl == idp) - .Select(ic => ic.Certificate), + + community == null ? + new List(){ _certificateStore.IssuedCertificates.First().Certificate } : + _certificateStore.IssuedCertificates.Where(ic => ic.Community == community) + .Select(ic => ic.Certificate), + OptionsMonitor.CurrentValue.Scope.ToSpaceSeparatedString(), Context.RequestAborted); 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..573b5bb8 100644 --- a/_tests/UdapServer.Tests/Common/TestExtensions.cs +++ b/_tests/UdapServer.Tests/Common/TestExtensions.cs @@ -1,13 +1,20 @@ -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 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 @@ -23,14 +30,33 @@ public static class TestExtensions public static AuthenticationBuilder AddTieredOAuthForTests( this AuthenticationBuilder builder, Action configuration, - UdapIdentityServerPipeline pipeline) + UdapIdentityServerPipeline pipelineIdp1, + UdapIdentityServerPipeline pipelineIdp2) { builder.Services.AddScoped(sp => - new UdapClient( - pipeline.BrowserClient, - sp.GetRequiredService(), - sp.GetRequiredService>(), - sp.GetRequiredService>())); + { + var dynamicIdp = sp.GetRequiredService(); + + if (dynamicIdp.Name == "https://idpserver") + { + return new UdapClient( + pipelineIdp1.BackChannelClient, + sp.GetRequiredService(), + sp.GetRequiredService>(), + sp.GetRequiredService>()); + } + + if (dynamicIdp?.Name == "https://idpserver2") + { + return new UdapClient( + pipelineIdp2.BackChannelClient, + sp.GetRequiredService(), + sp.GetRequiredService>(), + sp.GetRequiredService>()); + } + + return null; + }); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); diff --git a/_tests/UdapServer.Tests/Common/UdapAuthServerPipeline.cs b/_tests/UdapServer.Tests/Common/UdapAuthServerPipeline.cs index 63b93188..1a0babc5 100644 --- a/_tests/UdapServer.Tests/Common/UdapAuthServerPipeline.cs +++ b/_tests/UdapServer.Tests/Common/UdapAuthServerPipeline.cs @@ -39,6 +39,7 @@ using Udap.Common.Certificates; using Udap.Common.Models; using Udap.Server.Configuration.DependencyInjection; +using Udap.Server.Hosting.DynamicProviders.Store; using Udap.Server.Registration; using Udap.Server.ResponseHandling; using Udap.Server.Security.Authentication.TieredOAuth; @@ -77,6 +78,7 @@ public class UdapAuthServerPipeline public IdentityServerOptions Options { get; set; } public List Clients { get; set; } = new List(); + public List OidcProviders { 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(); @@ -120,7 +122,7 @@ public void Initialize(string basePath = null, bool enableLogging = false) } }); - builder.ConfigureAppConfiguration(configure => configure.AddJsonFile("appsettings.Test.json")); + builder.ConfigureAppConfiguration(configure => configure.AddJsonFile("appsettings.Auth.json")); if (enableLogging) { @@ -146,6 +148,8 @@ public void ConfigureServices(WebHostBuilderContext builder, IServiceCollection OnPreConfigureServices(builder, services); + services.AddSingleton(); + // services.AddAuthentication(opts => // { // opts.AddScheme("external", scheme => @@ -191,6 +195,9 @@ public void ConfigureServices(WebHostBuilderContext builder, IServiceCollection .AddInMemoryIdentityResources(IdentityScopes) .AddInMemoryApiResources(ApiResources) .AddTestUsers(Users) + .AddInMemoryOidcProviders(OidcProviders) + .AddInMemoryCaching() + .AddIdentityProviderStoreCache() .AddDeveloperSigningCredential(persistKey: false); @@ -647,4 +654,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 529fd8cc..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.Test.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/Tiered/TieredOauthTests.cs b/_tests/UdapServer.Tests/Conformance/Tiered/TieredOauthTests.cs index 984b715e..261e47cc 100644 --- a/_tests/UdapServer.Tests/Conformance/Tiered/TieredOauthTests.cs +++ b/_tests/UdapServer.Tests/Conformance/Tiered/TieredOauthTests.cs @@ -16,10 +16,13 @@ using System.Text; using System.Text.Json; using Duende.IdentityServer; +using Duende.IdentityServer.EntityFramework.Stores; using Duende.IdentityServer.Models; +using Duende.IdentityServer.Stores; 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; @@ -51,7 +54,8 @@ public class TieredOauthTests private UdapAuthServerPipeline _mockAuthorServerPipeline = new UdapAuthServerPipeline(); private UdapIdentityServerPipeline _mockIdPPipeline = new UdapIdentityServerPipeline(); - + private UdapIdentityServerPipeline _mockIdPPipeline2 = new UdapIdentityServerPipeline("https://idpserver2", "appsettings.Idp2.json"); + private IAuthenticationSchemeProvider _schemeProvider; public TieredOauthTests(ITestOutputHelper testOutputHelper) @@ -60,11 +64,12 @@ public TieredOauthTests(ITestOutputHelper testOutputHelper) var sureFhirLabsAnchor = new X509Certificate2("CertStore/anchors/caLocalhostCert.cer"); var intermediateCert = new X509Certificate2("CertStore/intermediates/intermediateLocalhostCert.cer"); - var idpAnchor1 = new X509Certificate2("CertStore/anchors/caLocalhostCert.cer"); - var idpIntermediate1 = 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); + BuildUdapIdentityProvider(sureFhirLabsAnchor, intermediateCert); + BuildUdapIdentityProvider2(sureFhirLabsAnchor, intermediateCert); } private void BuildUdapAuthorizationServer(X509Certificate2 sureFhirLabsAnchor, X509Certificate2 intermediateCert) @@ -116,15 +121,40 @@ private void BuildUdapAuthorizationServer(X509Certificate2 sureFhirLabsAnchor, X options.AuthorizationEndpoint = "https://idpserver/connect/authorize"; options.TokenEndpoint = "https://idpserver/connect/token"; options.IdPBaseUrl = "https://idpserver"; - }, _mockIdPPipeline); // point backchannel to the IdP + }, + _mockIdPPipeline, + _mockIdPPipeline2); // point backchannel to the IdP services.AddAuthorization(); // required for TieredOAuth Testing + + services.ConfigureAll(options => + { + options.BackchannelHttpHandler = _mockIdPPipeline2.Server?.CreateHandler(); + }); + + var _oidcProviders = new List() + { + new OidcProvider + { + Scheme = "tieredOauth2", + Authority = "https://idpserver2?community=udap://idp-community-2", + ClientId = "client", + ClientSecret = "secret", + ResponseType = "code", + } + }; + + _mockAuthorServerPipeline.OidcProviders = _oidcProviders; + + + + using var serviceProvider = services.BuildServiceProvider(); _schemeProvider = serviceProvider.GetRequiredService(); - + }; _mockAuthorServerPipeline.OnPostConfigure += app => @@ -275,11 +305,91 @@ private void BuildUdapIdentityProvider(X509Certificate2 sureFhirLabsAnchor, X509 _mockIdPPipeline.Subject = new IdentityServerUser("bob").CreatePrincipal(); } + private void BuildUdapIdentityProvider2(X509Certificate2 sureFhirLabsAnchor, X509Certificate2 intermediateCert) + { + _mockIdPPipeline2.OnPostConfigureServices += s => + { + s.AddSingleton(new ServerSettings + { + ServerSupport = ServerSupport.UDAP, + DefaultUserScopes = "udap", + DefaultSystemScopes = "udap", + // ForceStateParamOnAuthorizationCode = false (default) + AlwaysIncludeUserClaimsInIdToken = true + }); + }; + + _mockIdPPipeline2.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(_mockIdPPipeline2.Clients); + }; + + _mockIdPPipeline2.Initialize(enableLogging: true); + _mockIdPPipeline2.BrowserClient.AllowAutoRedirect = false; + + _mockIdPPipeline2.Communities.Add(new Community + { + Name = "udap://fhirlabs.net", + 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, + Enabled = true, + Intermediates = new List() + { + new Intermediate + { + BeginDate = intermediateCert.NotBefore.ToUniversalTime(), + EndDate = intermediateCert.NotAfter.ToUniversalTime(), + Name = intermediateCert.Subject, + Certificate = intermediateCert.ToPemFormat(), + Thumbprint = intermediateCert.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 Claim[] + { + 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(); + } + /// /// /// /// - [Fact] + [Fact(Skip = "Dynamic Tiered OAuth Provider WIP")] public async Task ClientAuthorize_IdPDiscovery_IdPRegistration_IdPAuthAccess_ClientAuthAccess_Test() { // Register client with auth server @@ -292,10 +402,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(); @@ -570,6 +683,308 @@ public async Task ClientAuthorize_IdPDiscovery_IdPRegistration_IdPAuthAccess_Cli */ } + [Fact] + public async Task Tiered_OAuth_With_DynamicProvider() + { + // Register client with auth server + var resultDocument = await RegisterClientWithAuthServer(); + _mockAuthorServerPipeline.RemoveSessionCookie(); + _mockAuthorServerPipeline.RemoveLoginCookie(); + resultDocument.Should().NotBeNull(); + resultDocument!.ClientId.Should().NotBeNull(); + + var clientId = resultDocument.ClientId!; + + var dynamicIdp = _mockAuthorServerPipeline.ApplicationServices.GetRequiredService(); + dynamicIdp.Name = _mockIdPPipeline2.BaseUrl; + + ////////////////////// + // ClientAuthorize + ////////////////////// + + // Data Holder's Auth Server validates Identity Provider's Server software statement + + var clientState = Guid.NewGuid().ToString(); + + var clientAuthorizeUrl = _mockAuthorServerPipeline.CreateAuthorizeUrl( + clientId: clientId, + responseType: "code", + scope: "udap openid user/*.read", + redirectUri: "https://code_client/callback", + state: clientState, + extra: new + { + idp = "https://idpserver2?community=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=idp-community-2"); + + var schemes = await _schemeProvider.GetAllSchemesAsync(); + + var sb = new StringBuilder(); + sb.Append("https://server/externallogin/challenge?"); // built in UdapAccount/Login/Index.cshtml.cs + sb.Append("scheme=").Append(schemes.First().Name); + sb.Append("&returnUrl=").Append(Uri.EscapeDataString(returnUrl)); + clientAuthorizeUrl = sb.ToString(); + + ////////////////////////////////// + // + // IdPDiscovery + // IdPRegistration + // IdPAuthAccess + // + ////////////////////////////////// + + + // Auto Dynamic registration between Auth Server and Identity Provider happens here. + // /Challenge? + // ctx.ChallengeAsync -> launch registered scheme. In this case the TieredOauthAuthenticationHandler + // see: OnExternalLoginChallenge and Challenge(props, scheme) in ExternalLogin/Challenge.cshtml.cs or UdapTieredLogin/Challenge.cshtml.cs + // Backchannel + // Discovery + // Auto registration + // externalloging/challenge or in the Udap implementation it is the UdapAccount/Login/Index.cshtml.cs. XSRF cookie is set here. + + // *** We are here after the request to the IdPs /authorize call. If the client is registered already then Discovery and Reg is skipped *** + // + // Authentication request (/authorize?) + // User logs in at IdP + // Authentication response + // Token request + // Data Holder incorporates user input into authorization decision + // + + + + // response after discovery and registration + _mockAuthorServerPipeline.BrowserClient.AllowCookies = true; // Need to set the idsrv cookie so calls to /authorize will succeed + + _mockAuthorServerPipeline.BrowserClient.GetXsrfCookie("https://server/signin-tieredoauth", 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(); + + 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); + var backChannelClientId = QueryHelpers.ParseQuery(backChannelChallengeResponse.Headers.Location.Query).Single(p => p.Key == "client_id").Value.ToString(); + 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(); + + + 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/signin-tieredoauth?"); + + var backChannelCode = QueryHelpers.ParseQuery(authorizeCallbackResult.Headers.Location.Query).Single(p => p.Key == "code").Value.ToString(); + + // + // 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 /signin-tieredoauth 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/signin-tieredoauth?..." + 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.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(); + + + var udapClient = new UdapClient( + _mockAuthorServerPipeline.BrowserClient, + _mockAuthorServerPipeline.Resolve(), + _mockAuthorServerPipeline.Resolve>(), + _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); + + + using var jsonDocument = JsonDocument.Parse(jwt.Payload.SerializeToJson()); + var formattedStatement = JsonSerializer.Serialize( + jsonDocument, + new JsonSerializerOptions { WriteIndented = true } + ); + + var formattedHeader = Base64UrlEncoder.Decode(jwt.EncodedHeader); + + _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); + + 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? + + } + + [Fact] + public async Task LoadDynamicProvider() + { + var identityProviderStore = _mockAuthorServerPipeline.ApplicationServices.GetRequiredService(); + + var dynamicSchemes = (await identityProviderStore.GetAllSchemeNamesAsync()) + .Where(x => x.Enabled) + .Select(x => new ExternalProvider + { + AuthenticationScheme = x.Scheme, + DisplayName = x.DisplayName, + ReturnUrl = "https://code_client/callback" + }); + + dynamicSchemes.Count().Should().Be(1); + } + private async Task RegisterClientWithAuthServer() { var clientCert = new X509Certificate2("CertStore/issued/fhirLabsApiClientLocalhostCert.pfx", "udap-test"); @@ -615,3 +1030,13 @@ public async Task ClientAuthorize_IdPDiscovery_IdPRegistration_IdPAuthAccess_Cli return resultDocument; } } + +public class ExternalProvider +{ + public string DisplayName { get; set; } + public string AuthenticationScheme { get; set; } + + public string ReturnUrl { get; set; } + + public bool IsChosenIdp { get; set; } +} diff --git a/_tests/UdapServer.Tests/UdapServer.Tests.csproj b/_tests/UdapServer.Tests/UdapServer.Tests.csproj index df1835bc..ee13daa7 100644 --- a/_tests/UdapServer.Tests/UdapServer.Tests.csproj +++ b/_tests/UdapServer.Tests/UdapServer.Tests.csproj @@ -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/appsettings.Auth.json b/_tests/UdapServer.Tests/appsettings.Auth.json new file mode 100644 index 00000000..71f5c34f --- /dev/null +++ b/_tests/UdapServer.Tests/appsettings.Auth.json @@ -0,0 +1,47 @@ +{ + "ConnectionStrings": { + "DefaultConnection": "Data Source=Udap.Sqlite.db;" + }, + + + "UdapFileCertStoreManifest": { + "Communities": [ + { + "Name": "udap://idp-community-1", + "IdPBaseUrl": "https://idpserver", + "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", + "IdPBaseUrl": "https://idpserver2", + "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.Test.json b/_tests/UdapServer.Tests/appsettings.Idp1.json similarity index 95% rename from _tests/UdapServer.Tests/appsettings.Test.json rename to _tests/UdapServer.Tests/appsettings.Idp1.json index d995be40..ed29a98e 100644 --- a/_tests/UdapServer.Tests/appsettings.Test.json +++ b/_tests/UdapServer.Tests/appsettings.Idp1.json @@ -40,7 +40,6 @@ "Communities": [ { "Name": "udap://idp-community-1", - "IdPBaseUrl": "https://idpserver", "Anchors": [ { "FilePath": "CertStore/anchors/caLocalhostCert.cer" @@ -58,7 +57,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..404fd57f --- /dev/null +++ b/_tests/UdapServer.Tests/appsettings.Idp2.json @@ -0,0 +1,50 @@ +{ + "ConnectionStrings": { + "DefaultConnection": "Data Source=Udap.Sqlite.db;" + }, + + "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 From 10fbf8eebb7597cbf02a7d56233dc27270c0fc08 Mon Sep 17 00:00:00 2001 From: Joseph Shook Date: Thu, 12 Oct 2023 16:02:17 -0700 Subject: [PATCH 08/42] Working in progress retained for Dynamic Provider support for Tiered OAuth providers --- .../UdapServer.Tests/Conformance/Tiered/TieredOauthTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/_tests/UdapServer.Tests/Conformance/Tiered/TieredOauthTests.cs b/_tests/UdapServer.Tests/Conformance/Tiered/TieredOauthTests.cs index 261e47cc..097413b4 100644 --- a/_tests/UdapServer.Tests/Conformance/Tiered/TieredOauthTests.cs +++ b/_tests/UdapServer.Tests/Conformance/Tiered/TieredOauthTests.cs @@ -389,7 +389,7 @@ private void BuildUdapIdentityProvider2(X509Certificate2 sureFhirLabsAnchor, X50 /// /// /// - [Fact(Skip = "Dynamic Tiered OAuth Provider WIP")] + [Fact] public async Task ClientAuthorize_IdPDiscovery_IdPRegistration_IdPAuthAccess_ClientAuthAccess_Test() { // Register client with auth server @@ -683,7 +683,7 @@ public async Task ClientAuthorize_IdPDiscovery_IdPRegistration_IdPAuthAccess_Cli */ } - [Fact] + [Fact(Skip = "Dynamic Tiered OAuth Provider WIP")] public async Task Tiered_OAuth_With_DynamicProvider() { // Register client with auth server From 9d3e900e60569cf79dae11d65caae2bf8abe28e7 Mon Sep 17 00:00:00 2001 From: Joseph Shook Date: Fri, 13 Oct 2023 14:09:48 -0700 Subject: [PATCH 09/42] Working in progress retained for Dynamic Provider support for Tiered OAuth providers --- Udap.Client/Client/IUdapClient.cs | 2 + Udap.Client/Client/UdapClient.cs | 15 ++ .../UdapBuilderExtensions/UdapCore.cs | 33 ++++ .../TieredOAuthAuthenticationExtensions.cs | 2 +- .../TieredOAuthAuthenticationHandler.cs | 79 +++++---- .../UdapServer.Tests/Common/TestExtensions.cs | 47 +++++ .../Common/UdapAuthServerPipeline.cs | 29 +++- .../Conformance/Tiered/TieredOauthTests.cs | 160 +++++++++++------- _tests/UdapServer.Tests/appsettings.Idp1.json | 4 - _tests/UdapServer.Tests/appsettings.Idp2.json | 3 - 10 files changed, 269 insertions(+), 105 deletions(-) diff --git a/Udap.Client/Client/IUdapClient.cs b/Udap.Client/Client/IUdapClient.cs index 0799f825..b9d5bea6 100644 --- a/Udap.Client/Client/IUdapClient.cs +++ b/Udap.Client/Client/IUdapClient.cs @@ -54,4 +54,6 @@ Task RegisterTieredClient(string redirect 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..64d0a5b0 100644 --- a/Udap.Client/Client/UdapClient.cs +++ b/Udap.Client/Client/UdapClient.cs @@ -329,6 +329,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) { diff --git a/Udap.Server/Configuration/DependencyInjection/UdapBuilderExtensions/UdapCore.cs b/Udap.Server/Configuration/DependencyInjection/UdapBuilderExtensions/UdapCore.cs index 47eb7154..786b01a5 100644 --- a/Udap.Server/Configuration/DependencyInjection/UdapBuilderExtensions/UdapCore.cs +++ b/Udap.Server/Configuration/DependencyInjection/UdapBuilderExtensions/UdapCore.cs @@ -15,20 +15,25 @@ // 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.Options; using Udap.Server.ResponseHandling; +using Udap.Server.Security.Authentication.TieredOAuth; using Udap.Server.Stores; using Udap.Server.Validation; using Constants = Udap.Server.Constants; @@ -118,4 +123,32 @@ public static IUdapServiceBuilder AddPrivateFileStore(this IUdapServiceBuilder b return builder; } + + public static IUdapServiceBuilder AddTieredOAuthDynamicProvider(this IUdapServiceBuilder builder) + { + builder.Services.Configure(options => + { + // this associates the TieredOAuthAuthenticationHandler and options (TieredOAuthAuthenticationOptions) classes + // to the idp class (OidcProvider) and type value ("udap_oidc") from the identity provider store + options.DynamicProviders.AddProviderType("udap_oidc"); + }); + + builder.Services.TryAddTransient(); + + builder.Services.TryAddTransient(); + builder.Services.AddHttpClient().AddHttpMessageHandler(); + + builder.Services.TryAddSingleton(sp => + { + var handler = new UdapClientMessageHandler( + sp.GetRequiredService(), + sp.GetRequiredService>()); + + handler.InnerHandler = sp.GetRequiredService(); + + return handler; + }); + + return builder; + } } diff --git a/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthAuthenticationExtensions.cs b/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthAuthenticationExtensions.cs index 53af7340..c7a0175c 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 => diff --git a/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthAuthenticationHandler.cs b/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthAuthenticationHandler.cs index 1b0ee0aa..6d5a3da6 100644 --- a/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthAuthenticationHandler.cs +++ b/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthAuthenticationHandler.cs @@ -34,6 +34,7 @@ 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; @@ -46,7 +47,8 @@ public class TieredOAuthAuthenticationHandler : OAuthHandler _identityProviders; /// /// Initializes a new instance of . @@ -59,12 +61,15 @@ public TieredOAuthAuthenticationHandler( ISystemClock clock, IUdapClient udapClient, IPrivateCertificateStore certificateStore, - IServiceScopeFactory scopeFactory) : + IUdapClientRegistrationStore udapClientRegistrationStore, + IEnumerable identityProviders + ) : base(options, logger, encoder, clock) { _udapClient = udapClient; _certificateStore = certificateStore; - _scopeFactory = scopeFactory; + _udapClientRegistrationStore = udapClientRegistrationStore; + _identityProviders = identityProviders; } /// Constructs the OAuth challenge url. @@ -84,10 +89,17 @@ 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.Items[UdapConstants.Discovery.AuthorizationEndpoint] ?? + throw new InvalidOperationException("Missing IdP authorization endpoint."); + // Dynamic options + return QueryHelpers.AddQueryString(authEndpoint, queryStrings!); } @@ -384,14 +396,11 @@ protected override async Task ExchangeCodeAsync([NotNull] OA 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 idpClient = await _udapClientRegistrationStore.FindTieredClientById(clientId); var idpClientId = idpClient.ClientId; await _certificateStore.Resolve(); @@ -424,17 +433,17 @@ protected override async Task HandleChallengeAsync(AuthenticationProperties prop // Validate idp Server; var idpUri = new Uri(idpParam); - var community = (HttpUtility.ParseQueryString(idpUri.Query).GetValues("community") ?? Array.Empty()).LastOrDefault(); + var communityParam = (HttpUtility.ParseQueryString(idpUri.Query).GetValues("community") ?? Array.Empty()).LastOrDefault(); var idp = idpUri.OriginalString; - if (community != null) + if (communityParam != null) { if (idp.Contains($":{{idpUri.Port}}")) { - idp = $"{idpUri.Scheme}://{idpUri.Host}:{idpUri.Port}{idpUri.LocalPath}"; + idp = $"{idpUri.Scheme}{Uri.SchemeDelimiter}{idpUri.Host}:{idpUri.Port}{idpUri.LocalPath}"; } else { - idp = $"{idpUri.Scheme}://{idpUri.Host}{idpUri.LocalPath}"; + idp = $"{idpUri.Scheme}{Uri.SchemeDelimiter}{idpUri.Host}{idpUri.LocalPath}"; } } @@ -442,10 +451,10 @@ protected override async Task HandleChallengeAsync(AuthenticationProperties prop _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) { @@ -477,9 +486,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; @@ -493,17 +500,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; + var communityId = await _udapClientRegistrationStore.GetCommunityId(communityName, Context.RequestAborted); if (communityId == null) { @@ -521,9 +519,11 @@ protected override async Task HandleChallengeAsync(AuthenticationProperties prop var document = await _udapClient.RegisterTieredClient( resourceHolderRedirectUrl, - community == null ? - new List(){ _certificateStore.IssuedCertificates.First().Certificate } : - _certificateStore.IssuedCertificates.Where(ic => ic.Community == community) + communityParam == null + ? + new List(){ _certificateStore.IssuedCertificates.First().Certificate } + : + _certificateStore.IssuedCertificates.Where(ic => ic.Community == communityParam) .Select(ic => ic.Certificate), OptionsMonitor.CurrentValue.Scope.ToSpaceSeparatedString(), @@ -549,7 +549,14 @@ protected override async Task HandleChallengeAsync(AuthenticationProperties prop Enabled = true }; - await registrationStore.UpsertTieredClient(tieredClient, Context.RequestAborted); + await _udapClientRegistrationStore.UpsertTieredClient(tieredClient, Context.RequestAborted); + + _identityProviders.ToList().Add(new OidcProvider + { + Scheme = "joe", + + }); + } properties.SetString("client_id", idpClientId); diff --git a/_tests/UdapServer.Tests/Common/TestExtensions.cs b/_tests/UdapServer.Tests/Common/TestExtensions.cs index 573b5bb8..979cba3f 100644 --- a/_tests/UdapServer.Tests/Common/TestExtensions.cs +++ b/_tests/UdapServer.Tests/Common/TestExtensions.cs @@ -7,6 +7,8 @@ // */ #endregion +using Duende.IdentityServer.Configuration; +using Duende.IdentityServer.Models; using Microsoft.AspNetCore.Authentication; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -66,4 +68,49 @@ public static AuthenticationBuilder AddTieredOAuthForTests( TieredOAuthAuthenticationDefaults.DisplayName, configuration); } + + public static IServiceCollection AddTieredOAuthDynamicProviderForTests( + this IServiceCollection services, + UdapIdentityServerPipeline pipelineIdp1, + UdapIdentityServerPipeline pipelineIdp2) + { + services.Configure(options => + { + // this associates the TieredOAuthAuthenticationHandler and options (TieredOAuthAuthenticationOptions) classes + // to the idp class (OidcProvider) and type value ("udap_oidc") from the identity provider store + options.DynamicProviders.AddProviderType("udap_oidc"); + }); + + services.TryAddTransient(); + + services.AddScoped(sp => + { + var dynamicIdp = sp.GetRequiredService(); + + if (dynamicIdp.Name == "https://idpserver") + { + return new UdapClient( + pipelineIdp1.BackChannelClient, + sp.GetRequiredService(), + sp.GetRequiredService>(), + sp.GetRequiredService>()); + } + + if (dynamicIdp?.Name == "https://idpserver2") + { + return new UdapClient( + pipelineIdp2.BackChannelClient, + sp.GetRequiredService(), + sp.GetRequiredService>(), + sp.GetRequiredService>()); + } + + return null; + }); + + services.TryAddSingleton(); + services.TryAddSingleton(); + + return services; + } } diff --git a/_tests/UdapServer.Tests/Common/UdapAuthServerPipeline.cs b/_tests/UdapServer.Tests/Common/UdapAuthServerPipeline.cs index 1a0babc5..1641d00f 100644 --- a/_tests/UdapServer.Tests/Common/UdapAuthServerPipeline.cs +++ b/_tests/UdapServer.Tests/Common/UdapAuthServerPipeline.cs @@ -13,6 +13,7 @@ using System.Net; using System.Security.Claims; using System.Security.Cryptography.X509Certificates; +using System.Web; using Duende.IdentityServer; using Duende.IdentityServer.Configuration; using Duende.IdentityServer.Events; @@ -21,6 +22,7 @@ using Duende.IdentityServer.Services; using Duende.IdentityServer.Test; using FluentAssertions; +using Google.Api; using IdentityModel; using IdentityModel.Client; using Microsoft.AspNetCore.Authentication; @@ -35,9 +37,11 @@ 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.Model; using Udap.Server.Configuration.DependencyInjection; using Udap.Server.Hosting.DynamicProviders.Store; using Udap.Server.Registration; @@ -175,6 +179,7 @@ public void ConfigureServices(WebHostBuilderContext builder, IServiceCollection .AddUdapInMemoryApiScopes(ApiScopes) .AddInMemoryUdapCertificates(Communities) .AddUdapResponseGenerators(); + // .AddTieredOAuthDynamicProvider(); //.AddSmartV2Expander(); @@ -305,7 +310,7 @@ private async Task OnExternalLoginChallenge(HttpContext ctx) throw new Exception("invalid return URL"); } - var scheme = ctx.Request.Query["scheme"].FirstOrDefault(); + var scheme = ctx.Request.Query["scheme"]; ; var props = new AuthenticationProperties { @@ -318,8 +323,28 @@ private async Task OnExternalLoginChallenge(HttpContext ctx) } }; + + // var identityProviders = ctx.RequestServices.GetRequiredService>(); + // var schemProvider = ctx.RequestServices.GetRequiredService(); + + var _udapClient = ctx.RequestServices.GetRequiredService(); + var originalRequestParams = HttpUtility.ParseQueryString(returnUrl); + var idp = (originalRequestParams.GetValues("idp") ?? throw new InvalidOperationException()).Last(); + var idpUri = new Uri(idp); + var request = new DiscoveryDocumentRequest + { + Address = idpUri.Scheme + Uri.SchemeDelimiter + idpUri.Host + idpUri.LocalPath, + Policy = new IdentityModel.Client.DiscoveryPolicy() + { + EndpointValidationExcludeList = new List { OidcConstants.Discovery.RegistrationEndpoint } + } + }; + + var openIdConfig = await _udapClient.ResolveOpenIdConfig(request); + props.Items.Add(UdapConstants.Discovery.AuthorizationEndpoint, openIdConfig.AuthorizeEndpoint); + // 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) diff --git a/_tests/UdapServer.Tests/Conformance/Tiered/TieredOauthTests.cs b/_tests/UdapServer.Tests/Conformance/Tiered/TieredOauthTests.cs index 097413b4..9a79c797 100644 --- a/_tests/UdapServer.Tests/Conformance/Tiered/TieredOauthTests.cs +++ b/_tests/UdapServer.Tests/Conformance/Tiered/TieredOauthTests.cs @@ -21,6 +21,7 @@ using Duende.IdentityServer.Stores; using Duende.IdentityServer.Test; using FluentAssertions; +using IdentityModel; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.OpenIdConnect; using Microsoft.AspNetCore.Builder; @@ -55,24 +56,27 @@ public class TieredOauthTests private UdapAuthServerPipeline _mockAuthorServerPipeline = new UdapAuthServerPipeline(); private UdapIdentityServerPipeline _mockIdPPipeline = new UdapIdentityServerPipeline(); private UdapIdentityServerPipeline _mockIdPPipeline2 = new UdapIdentityServerPipeline("https://idpserver2", "appsettings.Idp2.json"); - - private IAuthenticationSchemeProvider _schemeProvider; + + private X509Certificate2 _community1Anchor; + private X509Certificate2 _community1IntermediateCert; + private X509Certificate2 _community2Anchor; + private 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"); + _community2Anchor = new X509Certificate2("CertStore/anchors/caLocalhostCert2.cer"); + _community2IntermediateCert = new X509Certificate2("CertStore/intermediates/intermediateLocalhostCert2.cer"); - BuildUdapAuthorizationServer(sureFhirLabsAnchor, intermediateCert); - BuildUdapIdentityProvider(sureFhirLabsAnchor, intermediateCert); - BuildUdapIdentityProvider2(sureFhirLabsAnchor, intermediateCert); + BuildUdapAuthorizationServer(); + BuildUdapIdentityProvider1(); + BuildUdapIdentityProvider2(); } - private void BuildUdapAuthorizationServer(X509Certificate2 sureFhirLabsAnchor, X509Certificate2 intermediateCert) + private void BuildUdapAuthorizationServer() { _mockAuthorServerPipeline.OnPostConfigureServices += s => { @@ -125,7 +129,11 @@ private void BuildUdapAuthorizationServer(X509Certificate2 sureFhirLabsAnchor, X _mockIdPPipeline, _mockIdPPipeline2); // point backchannel to the IdP - + + + services.AddTieredOAuthDynamicProviderForTests(_mockIdPPipeline, _mockIdPPipeline2); + + services.AddAuthorization(); // required for TieredOAuth Testing @@ -143,6 +151,7 @@ private void BuildUdapAuthorizationServer(X509Certificate2 sureFhirLabsAnchor, X ClientId = "client", ClientSecret = "secret", ResponseType = "code", + Type = "udap_oidc", } }; @@ -151,9 +160,7 @@ private void BuildUdapAuthorizationServer(X509Certificate2 sureFhirLabsAnchor, X - using var serviceProvider = services.BuildServiceProvider(); - - _schemeProvider = serviceProvider.GetRequiredService(); + // using var serviceProvider = services.BuildServiceProvider(); }; @@ -170,29 +177,63 @@ 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 = _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 + { + BeginDate = _community1IntermediateCert.NotBefore.ToUniversalTime(), + EndDate = _community1IntermediateCert.NotAfter.ToUniversalTime(), + Name = _community1IntermediateCert.Subject, + Certificate = _community1IntermediateCert.ToPemFormat(), + Thumbprint = _community1IntermediateCert.Thumbprint, + Enabled = true + } + } + } + } + }); + + _mockAuthorServerPipeline.Communities.Add(new Community + { + Id = 1, + Name = "udap://idp-community-2", 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 = _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 Intermediate { - BeginDate = intermediateCert.NotBefore.ToUniversalTime(), - EndDate = intermediateCert.NotAfter.ToUniversalTime(), - Name = intermediateCert.Subject, - Certificate = intermediateCert.ToPemFormat(), - Thumbprint = intermediateCert.Thumbprint, + BeginDate = _community2IntermediateCert.NotBefore.ToUniversalTime(), + EndDate = _community2IntermediateCert.NotAfter.ToUniversalTime(), + Name = _community2IntermediateCert.Subject, + Certificate = _community2IntermediateCert.ToPemFormat(), + Thumbprint = _community2IntermediateCert.Thumbprint, Enabled = true } } @@ -200,10 +241,10 @@ private void BuildUdapAuthorizationServer(X509Certificate2 sureFhirLabsAnchor, X } }); - + // _mockAuthorServerPipeline. - + _mockAuthorServerPipeline.IdentityScopes.Add(new IdentityResources.OpenId()); _mockAuthorServerPipeline.IdentityScopes.Add(new IdentityResources.Profile()); _mockAuthorServerPipeline.IdentityScopes.Add(new UdapIdentityResources.Udap()); @@ -225,7 +266,7 @@ 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 => { @@ -252,29 +293,29 @@ private void BuildUdapIdentityProvider(X509Certificate2 sureFhirLabsAnchor, X509 _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 { - 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 } } @@ -305,7 +346,7 @@ private void BuildUdapIdentityProvider(X509Certificate2 sureFhirLabsAnchor, X509 _mockIdPPipeline.Subject = new IdentityServerUser("bob").CreatePrincipal(); } - private void BuildUdapIdentityProvider2(X509Certificate2 sureFhirLabsAnchor, X509Certificate2 intermediateCert) + private void BuildUdapIdentityProvider2() { _mockIdPPipeline2.OnPostConfigureServices += s => { @@ -332,29 +373,29 @@ private void BuildUdapIdentityProvider2(X509Certificate2 sureFhirLabsAnchor, X50 _mockIdPPipeline2.Communities.Add(new Community { - Name = "udap://fhirlabs.net", + Name = "udap://idp-community-2", 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 = _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 Intermediate { - BeginDate = intermediateCert.NotBefore.ToUniversalTime(), - EndDate = intermediateCert.NotAfter.ToUniversalTime(), - Name = intermediateCert.Subject, - Certificate = intermediateCert.ToPemFormat(), - Thumbprint = intermediateCert.Thumbprint, + BeginDate = _community2IntermediateCert.NotBefore.ToUniversalTime(), + EndDate = _community2IntermediateCert.NotAfter.ToUniversalTime(), + Name = _community2IntermediateCert.Subject, + Certificate = _community2IntermediateCert.ToPemFormat(), + Thumbprint = _community2IntermediateCert.Thumbprint, Enabled = true } } @@ -447,7 +488,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 @@ -714,7 +755,7 @@ public async Task Tiered_OAuth_With_DynamicProvider() state: clientState, extra: new { - idp = "https://idpserver2?community=idp-community-2" + idp = "https://idpserver2?community=udap://idp-community-2" }); _mockAuthorServerPipeline.BrowserClient.AllowAutoRedirect = false; @@ -738,13 +779,14 @@ public async Task Tiered_OAuth_With_DynamicProvider() 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=idp-community-2"); + queryParams.Single(q => q.Key == "idp").Value.Should().BeEquivalentTo("https://idpserver2?community=udap://idp-community-2"); + + var schemes = await _mockAuthorServerPipeline.Resolve().GetAllSchemeNamesAsync(); - var schemes = await _schemeProvider.GetAllSchemesAsync(); var sb = new StringBuilder(); sb.Append("https://server/externallogin/challenge?"); // built in UdapAccount/Login/Index.cshtml.cs - sb.Append("scheme=").Append(schemes.First().Name); + sb.Append("scheme=").Append(schemes.First().Scheme); sb.Append("&returnUrl=").Append(Uri.EscapeDataString(returnUrl)); clientAuthorizeUrl = sb.ToString(); diff --git a/_tests/UdapServer.Tests/appsettings.Idp1.json b/_tests/UdapServer.Tests/appsettings.Idp1.json index ed29a98e..af8682e1 100644 --- a/_tests/UdapServer.Tests/appsettings.Idp1.json +++ b/_tests/UdapServer.Tests/appsettings.Idp1.json @@ -1,8 +1,4 @@ { - "ConnectionStrings": { - "DefaultConnection": "Data Source=Udap.Sqlite.db;" - }, - "UdapMetadataOptions": { "Enabled": true, diff --git a/_tests/UdapServer.Tests/appsettings.Idp2.json b/_tests/UdapServer.Tests/appsettings.Idp2.json index 404fd57f..62c61c5a 100644 --- a/_tests/UdapServer.Tests/appsettings.Idp2.json +++ b/_tests/UdapServer.Tests/appsettings.Idp2.json @@ -1,7 +1,4 @@ { - "ConnectionStrings": { - "DefaultConnection": "Data Source=Udap.Sqlite.db;" - }, "UdapMetadataOptions": { "Enabled": true, From 4e789cf3ed29769d2f7add8c90462b5130afb96e Mon Sep 17 00:00:00 2001 From: Joseph Shook Date: Mon, 16 Oct 2023 13:15:14 -0700 Subject: [PATCH 10/42] UDAP Dynamic Provider tests are mechanically functional Need to commit code to plant a stake. It is not 100% done. I still need a OidcProvider registered as a template to hook TieredOAuthenticationHandler. I have not persisted it yet. Need to extend the integration test to persist and then re-run process asserting the provider is persisted. Persistence in Duende IdentityServer means persisting the provider data to the Provider table. One noteworthy friction point: I have to set the Scope property of the templated OidcProvider. Even though the TieredOAuthAuthenticationOptions is registered and preset to scopes openid, email and profile the Duende mechanism for dynamically loading profiles changes the Scope property via post configuration process just before I go to use it. It seems like a bug, but would take some time to build a simple PR or why. But in the mean time setting the Scope in the OidcProvider template instance resolves the concern. All in all there is a lot to keep in your head to develop this dynamic provider code. But the consumer experience when building out a UDAP enabled IdentityServer should prove to be easier to setup than the current implementation. --- Directory.Packages.props | 1 + .../HttpClientTokenRequestExtensions.cs | 8 +- Udap.Model/Udap.Model.csproj | 21 ++++- .../UdapBuilderExtensions/UdapCore.cs | 31 +------- .../Oidc/UdapOidcConfigureOptions.cs | 78 +++++++++++++++++++ .../Oidc/UdapServerBuilderOidcExtensions.cs | 72 +++++++++++++++++ .../TieredOAuthAuthenticationExtensions.cs | 2 +- .../TieredOAuthAuthenticationHandler.cs | 39 +++++++++- .../TieredOAuthAuthenticationOptions.cs | 5 +- .../TieredOAuthPostConfigureOptions.cs | 41 +++++++++- Udap.sln | 12 +++ .../UdapServer.Tests/Common/TestExtensions.cs | 24 ++++++ .../Common/UdapAuthServerPipeline.cs | 23 +++++- .../Conformance/Tiered/TieredOauthTests.cs | 63 +++++++-------- .../clients/UdapEd/Directory.Packages.props | 2 +- examples/clients/UdapEd/Server/Dockerfile | 5 +- 16 files changed, 350 insertions(+), 77 deletions(-) create mode 100644 Udap.Server/Hosting/DynamicProviders/Oidc/UdapOidcConfigureOptions.cs create mode 100644 Udap.Server/Hosting/DynamicProviders/Oidc/UdapServerBuilderOidcExtensions.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 95071074..6c73eea5 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -4,6 +4,7 @@ + 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.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.Server/Configuration/DependencyInjection/UdapBuilderExtensions/UdapCore.cs b/Udap.Server/Configuration/DependencyInjection/UdapBuilderExtensions/UdapCore.cs index 786b01a5..606e678b 100644 --- a/Udap.Server/Configuration/DependencyInjection/UdapBuilderExtensions/UdapCore.cs +++ b/Udap.Server/Configuration/DependencyInjection/UdapBuilderExtensions/UdapCore.cs @@ -31,6 +31,7 @@ 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; @@ -123,32 +124,8 @@ public static IUdapServiceBuilder AddPrivateFileStore(this IUdapServiceBuilder b return builder; } +} - public static IUdapServiceBuilder AddTieredOAuthDynamicProvider(this IUdapServiceBuilder builder) - { - builder.Services.Configure(options => - { - // this associates the TieredOAuthAuthenticationHandler and options (TieredOAuthAuthenticationOptions) classes - // to the idp class (OidcProvider) and type value ("udap_oidc") from the identity provider store - options.DynamicProviders.AddProviderType("udap_oidc"); - }); - - builder.Services.TryAddTransient(); - - builder.Services.TryAddTransient(); - builder.Services.AddHttpClient().AddHttpMessageHandler(); - - builder.Services.TryAddSingleton(sp => - { - var handler = new UdapClientMessageHandler( - sp.GetRequiredService(), - sp.GetRequiredService>()); - - handler.InnerHandler = sp.GetRequiredService(); - - return handler; - }); - - return builder; - } +public class OidcProvUdapProviderider +{ } diff --git a/Udap.Server/Hosting/DynamicProviders/Oidc/UdapOidcConfigureOptions.cs b/Udap.Server/Hosting/DynamicProviders/Oidc/UdapOidcConfigureOptions.cs new file mode 100644 index 00000000..96d129ba --- /dev/null +++ b/Udap.Server/Hosting/DynamicProviders/Oidc/UdapOidcConfigureOptions.cs @@ -0,0 +1,78 @@ +#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.Hosting.DynamicProviders; +using Duende.IdentityServer.Models; +using IdentityModel; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Udap.Server.Security.Authentication.TieredOAuth; + +namespace Udap.Server.Hosting.DynamicProviders.Oidc; +public class UdapOidcConfigureOptions : ConfigureAuthenticationOptions +{ + /// + /// Allows for configuring the handler options from the identity provider configuration. + /// + /// + public UdapOidcConfigureOptions(IHttpContextAccessor httpContextAccessor, ILogger logger) : base(httpContextAccessor, logger) + { + } + + protected override void Configure(ConfigureAuthenticationContext context) + { + context.AuthenticationOptions.SignInScheme = context.DynamicProviderOptions.SignInScheme; + // context.AuthenticationOptions.SignOutScheme = context.DynamicProviderOptions.SignOutScheme; + + // context.AuthenticationOptions.Authority = context.IdentityProvider.Authority; + + // + // When this is the first time the idp is contacted then this property will be empty until it is dynamically + // registered and placed in the Provider table. + // The razor page that calls the HttpContext.ChallengeAsync method adds the authorization endpoint + // when it request the idp servers OpenId configuration. + // + context.AuthenticationOptions.AuthorizationEndpoint = context.IdentityProvider.Authority == "template" ? string.Empty : context.IdentityProvider.Authority; + + // context.AuthenticationOptions.RequireHttpsMetadata = context.IdentityProvider.Authority.StartsWith("https"); + + context.AuthenticationOptions.ClientId = context.IdentityProvider.ClientId; + context.AuthenticationOptions.ClientSecret = context.IdentityProvider.ClientSecret; + + // context.AuthenticationOptions.ResponseType = context.IdentityProvider.ResponseType; + // context.AuthenticationOptions.ResponseMode = + // context.IdentityProvider.ResponseType.Contains("id_token") ? "form_post" : "query"; + + context.AuthenticationOptions.UsePkce = context.IdentityProvider.UsePkce; + + context.AuthenticationOptions.Scope.Clear(); + foreach (var scope in context.IdentityProvider.Scopes) + { + context.AuthenticationOptions.Scope.Add(scope); + } + + context.AuthenticationOptions.SaveTokens = true; + // context.AuthenticationOptions.GetClaimsFromUserInfoEndpoint = context.IdentityProvider.GetClaimsFromUserInfoEndpoint; + // context.AuthenticationOptions.DisableTelemetry = true; +#if NET5_0_OR_GREATER + // context.AuthenticationOptions.MapInboundClaims = false; +#else + context.AuthenticationOptions.SecurityTokenValidator = new JwtSecurityTokenHandler + { + MapInboundClaims = false + }; +#endif + context.AuthenticationOptions.TokenValidationParameters.NameClaimType = JwtClaimTypes.Name; + context.AuthenticationOptions.TokenValidationParameters.RoleClaimType = JwtClaimTypes.Role; + + context.AuthenticationOptions.CallbackPath = context.PathPrefix + "/signin"; + // context.AuthenticationOptions.SignedOutCallbackPath = context.PathPrefix + "/signout-callback"; + // context.AuthenticationOptions.RemoteSignOutPath = context.PathPrefix + "/signout"; + } +} diff --git a/Udap.Server/Hosting/DynamicProviders/Oidc/UdapServerBuilderOidcExtensions.cs b/Udap.Server/Hosting/DynamicProviders/Oidc/UdapServerBuilderOidcExtensions.cs new file mode 100644 index 00000000..f25c65d2 --- /dev/null +++ b/Udap.Server/Hosting/DynamicProviders/Oidc/UdapServerBuilderOidcExtensions.cs @@ -0,0 +1,72 @@ +#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.Configuration; +using Duende.IdentityServer.Models; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Udap.Client.Client; +using Udap.Model; +using Udap.Server.Security.Authentication.TieredOAuth; + +namespace Udap.Server.Hosting.DynamicProviders.Oidc; +public static class UdapServerBuilderOidcExtensions +{ + + /// Adds the OIDC dynamic provider feature build specifically for UDAP Tiered OAuth. + /// + /// + public static IUdapServiceBuilder AddTieredOAuthDynamicProvider(this IUdapServiceBuilder builder) + { + builder.Services.Configure(options => + { + // this associates the TieredOAuthAuthenticationHandler and options (TieredOAuthAuthenticationOptions) classes + // to the idp class (OidcProvider) and type value ("udap_oidc") from the identity provider store + options.DynamicProviders.AddProviderType("udap_oidc"); + }); + + + + + + // this registers the OidcConfigureOptions to build the TieredOAuthAuthenticationOptions from the OidcProvider data + builder.Services.AddSingleton, UdapOidcConfigureOptions>(); + + // this services from ASP.NET Core and are added manually since we're not using the + // AddOpenIdConnect helper that we'd normally use statically on the AddAuthentication. + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton, TieredOAuthPostConfigureOptions>()); + builder.Services.TryAddTransient(); + + + + + + builder.Services.TryAddTransient(); + builder.Services.AddHttpClient().AddHttpMessageHandler(); + + builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); + + + builder.Services.TryAddSingleton(sp => + { + var handler = new UdapClientMessageHandler( + sp.GetRequiredService(), + sp.GetRequiredService>()); + + handler.InnerHandler = sp.GetRequiredService(); + + return handler; + }); + + return builder; + } +} diff --git a/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthAuthenticationExtensions.cs b/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthAuthenticationExtensions.cs index c7a0175c..86a65967 100644 --- a/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthAuthenticationExtensions.cs +++ b/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthAuthenticationExtensions.cs @@ -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 6d5a3da6..470cc371 100644 --- a/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthAuthenticationHandler.cs +++ b/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthAuthenticationHandler.cs @@ -22,6 +22,7 @@ using IdentityModel.Client; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.OAuth; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.DependencyInjection; @@ -43,6 +44,7 @@ namespace Udap.Server.Security.Authentication.TieredOAuth; + public class TieredOAuthAuthenticationHandler : OAuthHandler { private readonly IUdapClient _udapClient; @@ -96,8 +98,30 @@ protected override string BuildChallengeUrl(AuthenticationProperties properties, return QueryHelpers.AddQueryString(Options.AuthorizationEndpoint, queryStrings!); } - var authEndpoint = properties.Items[UdapConstants.Discovery.AuthorizationEndpoint] ?? + 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!); } @@ -393,6 +417,9 @@ 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 = @@ -409,9 +436,15 @@ protected override async Task ExchangeCodeAsync([NotNull] OA var tokenRequestBuilder = AccessTokenRequestForAuthorizationCodeBuilder.Create( idpClientId, Options.TokenEndpoint, - _certificateStore.IssuedCertificates.Where(ic => ic.IdPBaseUrl == idp) + + 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); diff --git a/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthAuthenticationOptions.cs b/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthAuthenticationOptions.cs index a7df3c8c..116f053b 100644 --- a/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthAuthenticationOptions.cs +++ b/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthAuthenticationOptions.cs @@ -15,7 +15,8 @@ namespace Udap.Server.Security.Authentication.TieredOAuth; -public class TieredOAuthAuthenticationOptions : OAuthOptions{ +public class TieredOAuthAuthenticationOptions : OAuthOptions +{ private readonly JwtSecurityTokenHandler _defaultHandler = new JwtSecurityTokenHandler(); @@ -54,4 +55,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/TieredOAuthPostConfigureOptions.cs b/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthPostConfigureOptions.cs index e1e2413b..a3ac9a36 100644 --- a/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthPostConfigureOptions.cs +++ b/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthPostConfigureOptions.cs @@ -7,8 +7,12 @@ // */ #endregion +using Microsoft.AspNetCore.Authentication.OpenIdConnect; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.DataProtection; using Microsoft.Extensions.Options; using Udap.Client.Client; +using System.Reflection.Metadata; namespace Udap.Server.Security.Authentication.TieredOAuth; @@ -16,14 +20,16 @@ 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; } /// @@ -37,5 +43,38 @@ public void PostConfigure(string? name, TieredOAuthAuthenticationOptions options options.BackchannelHttpHandler = _udapClientMessageHandler; options.SignInScheme = options.SignInScheme; + options.DataProtectionProvider ??= _dataProtection; + + + + if (options.StateDataFormat == null) + { + var dataProtector = options.DataProtectionProvider.CreateProtector( + typeof(TieredOAuthAuthenticationHandler).FullName!, name, "v1"); + 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.sln b/Udap.sln index 901baa15..1ae266ff 100644 --- a/Udap.sln +++ b/Udap.sln @@ -92,6 +92,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Udap.Auth.Server.Admin", "e EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Udap.Auth.Server", "examples\Udap.Auth.Server\Udap.Auth.Server.csproj", "{FD9FBB5C-DBCF-4C1D-9E21-A98485D179E6}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duende.IdentityServer", "..\..\..\DuendeSoftware\IdentityServer\src\IdentityServer\Duende.IdentityServer.csproj", "{A04357D7-35CA-42CC-AC76-69F2BEE26B09}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duende.IdentityServer.Configuration", "..\..\..\DuendeSoftware\IdentityServer\src\Configuration\Duende.IdentityServer.Configuration.csproj", "{CE954071-37A0-46B6-928B-30657717C406}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -206,6 +210,14 @@ Global {FD9FBB5C-DBCF-4C1D-9E21-A98485D179E6}.Debug|Any CPU.Build.0 = Debug|Any CPU {FD9FBB5C-DBCF-4C1D-9E21-A98485D179E6}.Release|Any CPU.ActiveCfg = Release|Any CPU {FD9FBB5C-DBCF-4C1D-9E21-A98485D179E6}.Release|Any CPU.Build.0 = Release|Any CPU + {A04357D7-35CA-42CC-AC76-69F2BEE26B09}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A04357D7-35CA-42CC-AC76-69F2BEE26B09}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A04357D7-35CA-42CC-AC76-69F2BEE26B09}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A04357D7-35CA-42CC-AC76-69F2BEE26B09}.Release|Any CPU.Build.0 = Release|Any CPU + {CE954071-37A0-46B6-928B-30657717C406}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CE954071-37A0-46B6-928B-30657717C406}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CE954071-37A0-46B6-928B-30657717C406}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CE954071-37A0-46B6-928B-30657717C406}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/_tests/UdapServer.Tests/Common/TestExtensions.cs b/_tests/UdapServer.Tests/Common/TestExtensions.cs index 979cba3f..af70b883 100644 --- a/_tests/UdapServer.Tests/Common/TestExtensions.cs +++ b/_tests/UdapServer.Tests/Common/TestExtensions.cs @@ -10,12 +10,15 @@ using Duende.IdentityServer.Configuration; using Duende.IdentityServer.Models; using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Udap.Client.Client; using Udap.Client.Configuration; +using Udap.Model; +using Udap.Server.Hosting.DynamicProviders.Oidc; using Udap.Server.Security.Authentication.TieredOAuth; namespace UdapServer.Tests.Common; @@ -81,8 +84,29 @@ public static IServiceCollection AddTieredOAuthDynamicProviderForTests( options.DynamicProviders.AddProviderType("udap_oidc"); }); + + + + + + // this registers the OidcConfigureOptions to build the TieredOAuthAuthenticationOptions from the OidcProvider data + services.AddSingleton, UdapOidcConfigureOptions>(); + + + services.TryAddEnumerable(ServiceDescriptor.Singleton, TieredOAuthPostConfigureOptions>()); + services.TryAddTransient(); + + + services.TryAddSingleton, TieredOAuthPostConfigureOptions>(); + + + + + + + services.AddScoped(sp => { var dynamicIdp = sp.GetRequiredService(); diff --git a/_tests/UdapServer.Tests/Common/UdapAuthServerPipeline.cs b/_tests/UdapServer.Tests/Common/UdapAuthServerPipeline.cs index 1641d00f..b7c36b0b 100644 --- a/_tests/UdapServer.Tests/Common/UdapAuthServerPipeline.cs +++ b/_tests/UdapServer.Tests/Common/UdapAuthServerPipeline.cs @@ -43,6 +43,7 @@ using Udap.Common.Models; using Udap.Model; using Udap.Server.Configuration.DependencyInjection; +using Udap.Server.Hosting.DynamicProviders.Oidc; using Udap.Server.Hosting.DynamicProviders.Store; using Udap.Server.Registration; using Udap.Server.ResponseHandling; @@ -324,16 +325,25 @@ private async Task OnExternalLoginChallenge(HttpContext ctx) }; - // var identityProviders = ctx.RequestServices.GetRequiredService>(); - // var schemProvider = ctx.RequestServices.GetRequiredService(); + var identityProviders = ctx.RequestServices.GetRequiredService>(); + var schemProvider = ctx.RequestServices.GetRequiredService(); var _udapClient = ctx.RequestServices.GetRequiredService(); 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.Items.Add(UdapConstants.Community, parts[1]); // TODO can this be Parameters instead + } + var idpUri = new Uri(idp); + var idpBaseUrl = idpUri.Scheme + Uri.SchemeDelimiter + idpUri.Host + idpUri.LocalPath; var request = new DiscoveryDocumentRequest { - Address = idpUri.Scheme + Uri.SchemeDelimiter + idpUri.Host + idpUri.LocalPath, + Address = idpBaseUrl, Policy = new IdentityModel.Client.DiscoveryPolicy() { EndpointValidationExcludeList = new List { OidcConstants.Discovery.RegistrationEndpoint } @@ -341,7 +351,12 @@ private async Task OnExternalLoginChallenge(HttpContext ctx) }; var openIdConfig = await _udapClient.ResolveOpenIdConfig(request); - props.Items.Add(UdapConstants.Discovery.AuthorizationEndpoint, openIdConfig.AuthorizeEndpoint); + + // 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('/')); // When calling ChallengeAsync your handler will be called if it is registered. await ctx.ChallengeAsync(scheme, props); diff --git a/_tests/UdapServer.Tests/Conformance/Tiered/TieredOauthTests.cs b/_tests/UdapServer.Tests/Conformance/Tiered/TieredOauthTests.cs index 9a79c797..5b9cc153 100644 --- a/_tests/UdapServer.Tests/Conformance/Tiered/TieredOauthTests.cs +++ b/_tests/UdapServer.Tests/Conformance/Tiered/TieredOauthTests.cs @@ -115,21 +115,21 @@ private void BuildUdapAuthorizationServer() services.Configure(builderContext.Configuration.GetSection(Udap.Common.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, - _mockIdPPipeline2); // 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; + options.AuthorizationEndpoint = "https://idpserver/connect/authorize"; + options.TokenEndpoint = "https://idpserver/connect/token"; + options.IdPBaseUrl = "https://idpserver"; + }, + _mockIdPPipeline, + _mockIdPPipeline2); // point backchannel to the IdP - + ; services.AddTieredOAuthDynamicProviderForTests(_mockIdPPipeline, _mockIdPPipeline2); @@ -142,25 +142,25 @@ private void BuildUdapAuthorizationServer() options.BackchannelHttpHandler = _mockIdPPipeline2.Server?.CreateHandler(); }); + var _oidcProviders = new List() { new OidcProvider { - Scheme = "tieredOauth2", - Authority = "https://idpserver2?community=udap://idp-community-2", + Scheme = "idpserver2", + Authority = "template", //TODO: hoping I can remove this template idea and be purely dynamic. ClientId = "client", ClientSecret = "secret", - ResponseType = "code", Type = "udap_oidc", + UsePkce = false, + Scope = "openid email profile" } }; - + _mockAuthorServerPipeline.OidcProviders = _oidcProviders; - - - // using var serviceProvider = services.BuildServiceProvider(); + using var serviceProvider = services.BuildServiceProvider(); }; @@ -724,7 +724,7 @@ public async Task ClientAuthorize_IdPDiscovery_IdPRegistration_IdPAuthAccess_Cli */ } - [Fact(Skip = "Dynamic Tiered OAuth Provider WIP")] + [Fact] //(Skip = "Dynamic Tiered OAuth Provider WIP")] public async Task Tiered_OAuth_With_DynamicProvider() { // Register client with auth server @@ -821,10 +821,10 @@ public async Task Tiered_OAuth_With_DynamicProvider() // 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/idpserver2/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/idpserver2/signin", new TieredOAuthAuthenticationOptions().CorrelationCookie.Name).Should().NotBeNull(); backChannelChallengeResponse.StatusCode.Should().Be(HttpStatusCode.Redirect, await backChannelChallengeResponse.Content.ReadAsStringAsync()); backChannelChallengeResponse.Headers.Location.Should().NotBeNull(); @@ -859,9 +859,10 @@ public async Task Tiered_OAuth_With_DynamicProvider() // _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/idpserver2/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 @@ -876,7 +877,7 @@ public async Task Tiered_OAuth_With_DynamicProvider() _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/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: @@ -892,7 +893,7 @@ public async Task Tiered_OAuth_With_DynamicProvider() _mockAuthorServerPipeline.BrowserClient.AllowCookies = true; - // "https://server/signin-tieredoauth?..." + // "https://server/federation/idpserver2/signin?..." var schemeCallbackResult = await _mockAuthorServerPipeline.BrowserClient.GetAsync(authorizeCallbackResult.Headers.Location!.AbsoluteUri); @@ -902,14 +903,14 @@ public async Task Tiered_OAuth_With_DynamicProvider() // _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(); + // _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()); + _testOutputHelper.WriteLine(_mockIdPPipeline2.IdToken.ToString()); _mockIdPPipeline2.IdToken.Claims.Should().Contain(c => c.Type == "hl7_identifier"); _mockIdPPipeline2.IdToken.Claims.Single(c => c.Type == "hl7_identifier").Value.Should().Be("123"); 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/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 From 609e90a75f9b98dd18d78a4e94a6fb646f0105a6 Mon Sep 17 00:00:00 2001 From: Joseph Shook Date: Mon, 16 Oct 2023 14:41:48 -0700 Subject: [PATCH 11/42] Dynamic Tiered Provider wip --- .../TieredOAuthAuthenticationHandler.cs | 17 ++++------------- .../TieredOAuthAuthenticationOptions.cs | 12 ++++++++++-- Udap.sln | 12 ------------ .../Common/UdapAuthServerPipeline.cs | 6 +----- .../Conformance/Tiered/TieredOauthTests.cs | 7 ++++--- 5 files changed, 19 insertions(+), 35 deletions(-) diff --git a/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthAuthenticationHandler.cs b/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthAuthenticationHandler.cs index 470cc371..3e1a123d 100644 --- a/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthAuthenticationHandler.cs +++ b/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthAuthenticationHandler.cs @@ -50,7 +50,6 @@ public class TieredOAuthAuthenticationHandler : OAuthHandler _identityProviders; /// /// Initializes a new instance of . @@ -71,7 +70,6 @@ IEnumerable identityProviders _udapClient = udapClient; _certificateStore = certificateStore; _udapClientRegistrationStore = udapClientRegistrationStore; - _identityProviders = identityProviders; } /// Constructs the OAuth challenge url. @@ -93,10 +91,10 @@ protected override string BuildChallengeUrl(AuthenticationProperties properties, queryStrings.Add("state", state); // Static configured Options - if (!Options.AuthorizationEndpoint.IsNullOrEmpty()) - { - return QueryHelpers.AddQueryString(Options.AuthorizationEndpoint, queryStrings!); - } + // 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."); @@ -583,13 +581,6 @@ protected override async Task HandleChallengeAsync(AuthenticationProperties prop }; await _udapClientRegistrationStore.UpsertTieredClient(tieredClient, Context.RequestAborted); - - _identityProviders.ToList().Add(new OidcProvider - { - Scheme = "joe", - - }); - } properties.SetString("client_id", idpClientId); diff --git a/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthAuthenticationOptions.cs b/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthAuthenticationOptions.cs index 116f053b..17a8bd32 100644 --- a/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthAuthenticationOptions.cs +++ b/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthAuthenticationOptions.cs @@ -28,14 +28,22 @@ public TieredOAuthAuthenticationOptions() // 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; + + // + // Defaults to survive the IIdentityProviderConfigurationValidator + // All of these are set during the GET /externallogin/challenge by + // placing them in the AuthenticationProperties.Parameters + // + AuthorizationEndpoint = "/connect/authorize"; + TokenEndpoint = "/connect/token"; } /// diff --git a/Udap.sln b/Udap.sln index 1ae266ff..901baa15 100644 --- a/Udap.sln +++ b/Udap.sln @@ -92,10 +92,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Udap.Auth.Server.Admin", "e EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Udap.Auth.Server", "examples\Udap.Auth.Server\Udap.Auth.Server.csproj", "{FD9FBB5C-DBCF-4C1D-9E21-A98485D179E6}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duende.IdentityServer", "..\..\..\DuendeSoftware\IdentityServer\src\IdentityServer\Duende.IdentityServer.csproj", "{A04357D7-35CA-42CC-AC76-69F2BEE26B09}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duende.IdentityServer.Configuration", "..\..\..\DuendeSoftware\IdentityServer\src\Configuration\Duende.IdentityServer.Configuration.csproj", "{CE954071-37A0-46B6-928B-30657717C406}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -210,14 +206,6 @@ Global {FD9FBB5C-DBCF-4C1D-9E21-A98485D179E6}.Debug|Any CPU.Build.0 = Debug|Any CPU {FD9FBB5C-DBCF-4C1D-9E21-A98485D179E6}.Release|Any CPU.ActiveCfg = Release|Any CPU {FD9FBB5C-DBCF-4C1D-9E21-A98485D179E6}.Release|Any CPU.Build.0 = Release|Any CPU - {A04357D7-35CA-42CC-AC76-69F2BEE26B09}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A04357D7-35CA-42CC-AC76-69F2BEE26B09}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A04357D7-35CA-42CC-AC76-69F2BEE26B09}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A04357D7-35CA-42CC-AC76-69F2BEE26B09}.Release|Any CPU.Build.0 = Release|Any CPU - {CE954071-37A0-46B6-928B-30657717C406}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {CE954071-37A0-46B6-928B-30657717C406}.Debug|Any CPU.Build.0 = Debug|Any CPU - {CE954071-37A0-46B6-928B-30657717C406}.Release|Any CPU.ActiveCfg = Release|Any CPU - {CE954071-37A0-46B6-928B-30657717C406}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/_tests/UdapServer.Tests/Common/UdapAuthServerPipeline.cs b/_tests/UdapServer.Tests/Common/UdapAuthServerPipeline.cs index b7c36b0b..83247ec8 100644 --- a/_tests/UdapServer.Tests/Common/UdapAuthServerPipeline.cs +++ b/_tests/UdapServer.Tests/Common/UdapAuthServerPipeline.cs @@ -323,10 +323,6 @@ private async Task OnExternalLoginChallenge(HttpContext ctx) { "scheme", scheme }, } }; - - - var identityProviders = ctx.RequestServices.GetRequiredService>(); - var schemProvider = ctx.RequestServices.GetRequiredService(); var _udapClient = ctx.RequestServices.GetRequiredService(); var originalRequestParams = HttpUtility.ParseQueryString(returnUrl); @@ -336,7 +332,7 @@ private async Task OnExternalLoginChallenge(HttpContext ctx) if (parts.Length > 1) { - props.Items.Add(UdapConstants.Community, parts[1]); // TODO can this be Parameters instead + props.Parameters.Add(UdapConstants.Community, parts[1]); } var idpUri = new Uri(idp); diff --git a/_tests/UdapServer.Tests/Conformance/Tiered/TieredOauthTests.cs b/_tests/UdapServer.Tests/Conformance/Tiered/TieredOauthTests.cs index 5b9cc153..4ca67504 100644 --- a/_tests/UdapServer.Tests/Conformance/Tiered/TieredOauthTests.cs +++ b/_tests/UdapServer.Tests/Conformance/Tiered/TieredOauthTests.cs @@ -122,9 +122,9 @@ private void BuildUdapAuthorizationServer() .AddTieredOAuthForTests(options => { options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme; - options.AuthorizationEndpoint = "https://idpserver/connect/authorize"; - options.TokenEndpoint = "https://idpserver/connect/token"; - options.IdPBaseUrl = "https://idpserver"; + // options.AuthorizationEndpoint = "https://idpserver/connect/authorize"; + // options.TokenEndpoint = "https://idpserver/connect/token"; + // options.IdPBaseUrl = "https://idpserver"; }, _mockIdPPipeline, _mockIdPPipeline2); // point backchannel to the IdP @@ -1009,6 +1009,7 @@ public async Task Tiered_OAuth_With_DynamicProvider() // Todo: Validate claims. Like missing name and other identity claims. Maybe add a hl7_identifier // Why is idp:TieredOAuth in the returned claims? + } [Fact] From cb006d9f707dc409c7257f9d35f907a8bcd29c25 Mon Sep 17 00:00:00 2001 From: Joseph Shook Date: Mon, 16 Oct 2023 19:30:16 -0700 Subject: [PATCH 12/42] Update packages --- Directory.Packages.props | 6 +++--- .../TieredOAuthAuthenticationOptions.cs | 1 - _tests/Directory.Packages.props | 16 ++++++++-------- _tests/UdapServer.Tests/UdapServer.Tests.csproj | 2 +- examples/FhirLabsApi/FhirLabsApi.csproj | 2 +- .../Udap.Auth.Server.Admin.csproj | 8 ++++---- .../Udap.Auth.Server/Udap.Auth.Server.csproj | 6 +++--- examples/Udap.CA/Udap.CA.csproj | 8 ++++---- .../Udap.Identity.Provider.2.csproj | 6 +++--- .../Udap.Identity.Provider.csproj | 6 +++--- .../1_UdapClientMetadata.csproj | 2 +- .../2_UdapClientMetadata.csproj | 2 +- .../UdapEd/Client/Pages/UdapConsumer.razor.cs | 1 - .../clients/UdapEd/Client/UdapEd.Client.csproj | 8 ++++---- .../clients/UdapEd/Server/UdapEd.Server.csproj | 2 +- .../clients/UdapEd/Shared/UdapEd.Shared.csproj | 2 +- .../UdapDb.SqlServer/UdapDb.SqlServer.csproj | 6 +++--- 17 files changed, 41 insertions(+), 43 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 6c73eea5..56e7281b 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -7,10 +7,10 @@ - + - + @@ -22,7 +22,7 @@ - + diff --git a/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthAuthenticationOptions.cs b/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthAuthenticationOptions.cs index 17a8bd32..8a4c3866 100644 --- a/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthAuthenticationOptions.cs +++ b/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthAuthenticationOptions.cs @@ -11,7 +11,6 @@ using Microsoft.IdentityModel.Tokens; using System.IdentityModel.Tokens.Jwt; using IdentityModel; -using Udap.Model; namespace Udap.Server.Security.Authentication.TieredOAuth; diff --git a/_tests/Directory.Packages.props b/_tests/Directory.Packages.props index dc983d1d..1367be96 100644 --- a/_tests/Directory.Packages.props +++ b/_tests/Directory.Packages.props @@ -12,13 +12,13 @@ - + - - - - - + + + + + @@ -26,8 +26,8 @@ - - + + diff --git a/_tests/UdapServer.Tests/UdapServer.Tests.csproj b/_tests/UdapServer.Tests/UdapServer.Tests.csproj index ee13daa7..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 diff --git a/examples/FhirLabsApi/FhirLabsApi.csproj b/examples/FhirLabsApi/FhirLabsApi.csproj index 78448258..2d944469 100644 --- a/examples/FhirLabsApi/FhirLabsApi.csproj +++ b/examples/FhirLabsApi/FhirLabsApi.csproj @@ -47,7 +47,7 @@ - + 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..0a0a0158 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/Udap.Auth.Server.csproj b/examples/Udap.Auth.Server/Udap.Auth.Server.csproj index c84bb3da..2ecf2f85 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.CA/Udap.CA.csproj b/examples/Udap.CA/Udap.CA.csproj index 5b2a4b74..ffee8324 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/Udap.Identity.Provider.2.csproj b/examples/Udap.Identity.Provider.2/Udap.Identity.Provider.2.csproj index d2ff1240..b49701cd 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/Udap.Identity.Provider.csproj b/examples/Udap.Identity.Provider/Udap.Identity.Provider.csproj index cdd5e1c1..b141616e 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 - - + + diff --git a/examples/clients/1_UdapClientMetadata/1_UdapClientMetadata.csproj b/examples/clients/1_UdapClientMetadata/1_UdapClientMetadata.csproj index 71da34a2..0b50d648 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..86e29daa 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/UdapConsumer.razor.cs b/examples/clients/UdapEd/Client/Pages/UdapConsumer.razor.cs index c90fc1cf..a0b0b2bb 100644 --- a/examples/clients/UdapEd/Client/Pages/UdapConsumer.razor.cs +++ b/examples/clients/UdapEd/Client/Pages/UdapConsumer.razor.cs @@ -9,7 +9,6 @@ using System.IdentityModel.Tokens.Jwt; using System.Text; -using System.Text.Json; using IdentityModel; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Web; diff --git a/examples/clients/UdapEd/Client/UdapEd.Client.csproj b/examples/clients/UdapEd/Client/UdapEd.Client.csproj index ad137a68..5082d14f 100644 --- a/examples/clients/UdapEd/Client/UdapEd.Client.csproj +++ b/examples/clients/UdapEd/Client/UdapEd.Client.csproj @@ -1,4 +1,4 @@ - + net7.0 @@ -18,9 +18,9 @@ - - - + + + diff --git a/examples/clients/UdapEd/Server/UdapEd.Server.csproj b/examples/clients/UdapEd/Server/UdapEd.Server.csproj index 8b021d83..cad9a484 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/UdapEd.Shared.csproj b/examples/clients/UdapEd/Shared/UdapEd.Shared.csproj index aba56ad2..1b81887d 100644 --- a/examples/clients/UdapEd/Shared/UdapEd.Shared.csproj +++ b/examples/clients/UdapEd/Shared/UdapEd.Shared.csproj @@ -29,7 +29,7 @@ - + diff --git a/migrations/UdapDb.SqlServer/UdapDb.SqlServer.csproj b/migrations/UdapDb.SqlServer/UdapDb.SqlServer.csproj index c56ea8a9..0af87cd3 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 - - + + From e9bac91d92bc2e50f3800825a8ca69488415d9cf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Oct 2023 02:31:07 +0000 Subject: [PATCH 13/42] Bump Microsoft.EntityFrameworkCore.Design from 7.0.11 to 7.0.12 Bumps [Microsoft.EntityFrameworkCore.Design](https://github.com/dotnet/efcore) from 7.0.11 to 7.0.12. - [Release notes](https://github.com/dotnet/efcore/releases) - [Commits](https://github.com/dotnet/efcore/compare/v7.0.11...v7.0.12) --- updated-dependencies: - dependency-name: Microsoft.EntityFrameworkCore.Design dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Directory.Packages.props | 2 +- examples/Directory.Packages.props | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 56e7281b..da700599 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -23,7 +23,7 @@ - + diff --git a/examples/Directory.Packages.props b/examples/Directory.Packages.props index acb7a01b..bfad2104 100644 --- a/examples/Directory.Packages.props +++ b/examples/Directory.Packages.props @@ -9,7 +9,7 @@ - + From e074921c16798aed07c41a2e02f9fcdbfd0fdcf7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Oct 2023 02:31:12 +0000 Subject: [PATCH 14/42] Bump Microsoft.EntityFrameworkCore.Sqlite from 7.0.11 to 7.0.12 Bumps [Microsoft.EntityFrameworkCore.Sqlite](https://github.com/dotnet/efcore) from 7.0.11 to 7.0.12. - [Release notes](https://github.com/dotnet/efcore/releases) - [Commits](https://github.com/dotnet/efcore/compare/v7.0.11...v7.0.12) --- updated-dependencies: - dependency-name: Microsoft.EntityFrameworkCore.Sqlite dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Directory.Packages.props | 2 +- examples/Directory.Packages.props | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 56e7281b..7024ff19 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -26,7 +26,7 @@ - + diff --git a/examples/Directory.Packages.props b/examples/Directory.Packages.props index acb7a01b..b31334da 100644 --- a/examples/Directory.Packages.props +++ b/examples/Directory.Packages.props @@ -12,7 +12,7 @@ - + From 124349f4d9d5bb3fbfbc8288bdd71d3dafc6e86d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Oct 2023 12:25:14 +0000 Subject: [PATCH 15/42] Bump Microsoft.IdentityModel.JsonWebTokens from 7.0.2 to 7.0.3 Bumps [Microsoft.IdentityModel.JsonWebTokens](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet) from 7.0.2 to 7.0.3. - [Release notes](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/releases) - [Changelog](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/blob/dev/CHANGELOG.md) - [Commits](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/compare/7.0.2...7.0.3) --- updated-dependencies: - dependency-name: Microsoft.IdentityModel.JsonWebTokens dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Directory.Packages.props | 2 +- _tests/Directory.Packages.props | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 2dc929e5..89798b41 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -32,7 +32,7 @@ - + diff --git a/_tests/Directory.Packages.props b/_tests/Directory.Packages.props index 1367be96..50fe38ce 100644 --- a/_tests/Directory.Packages.props +++ b/_tests/Directory.Packages.props @@ -22,7 +22,7 @@ - + From d46e4e66535121e3da83b5ab87426f3a9cb8d7e2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Oct 2023 12:25:59 +0000 Subject: [PATCH 16/42] Bump Microsoft.EntityFrameworkCore.SqlServer from 7.0.11 to 7.0.12 Bumps [Microsoft.EntityFrameworkCore.SqlServer](https://github.com/dotnet/efcore) from 7.0.11 to 7.0.12. - [Release notes](https://github.com/dotnet/efcore/releases) - [Commits](https://github.com/dotnet/efcore/compare/v7.0.11...v7.0.12) --- updated-dependencies: - dependency-name: Microsoft.EntityFrameworkCore.SqlServer dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Directory.Packages.props | 2 +- examples/Directory.Packages.props | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 2dc929e5..9115eae2 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -24,7 +24,7 @@ - + diff --git a/examples/Directory.Packages.props b/examples/Directory.Packages.props index 0adc179a..06e46371 100644 --- a/examples/Directory.Packages.props +++ b/examples/Directory.Packages.props @@ -10,7 +10,7 @@ - + From 92ea4ea1589d1e3dc949c999fb8a4093d617584d Mon Sep 17 00:00:00 2001 From: Joseph Shook Date: Tue, 17 Oct 2023 07:19:11 -0700 Subject: [PATCH 17/42] UI updates Transparent icon copyright notice --- artwork/udap.logo.48x48-removebg-preview.png | Bin 0 -> 6044 bytes artwork/udap.logo.48x48.png | Bin 0 -> 6037 bytes .../Udap.Auth.Server/Pages/Shared/_Nav.cshtml | 8 ++++++-- .../Udap.Auth.Server/wwwroot/udap.logo.48x48.png | Bin 0 -> 4064 bytes .../Pages/Shared/_Nav.cshtml | 6 +++++- .../wwwroot/udap.logo.48x48.png | Bin 0 -> 4064 bytes .../Pages/Shared/_Nav.cshtml | 6 +++++- .../wwwroot/udap.logo.48x48.png | Bin 0 -> 4064 bytes 8 files changed, 16 insertions(+), 4 deletions(-) create mode 100644 artwork/udap.logo.48x48-removebg-preview.png create mode 100644 artwork/udap.logo.48x48.png create mode 100644 examples/Udap.Auth.Server/wwwroot/udap.logo.48x48.png create mode 100644 examples/Udap.Identity.Provider.2/wwwroot/udap.logo.48x48.png create mode 100644 examples/Udap.Identity.Provider/wwwroot/udap.logo.48x48.png diff --git a/artwork/udap.logo.48x48-removebg-preview.png b/artwork/udap.logo.48x48-removebg-preview.png new file mode 100644 index 0000000000000000000000000000000000000000..7b335f4cd54d2cd2be1bd703167239e646353057 GIT binary patch literal 6044 zcmV;N7h~v&P)Py1R!KxbRA@tOnhBVkWtH!L-@eq^)oV700wEy+BLrd|7A1hf1tZ}C%7_Ay>8+}3{p#EA^S)ITGWXsuPp7)N ztLi)FyyyJS|NKuYX_BUhO9AnX_)5Bu;`<;5L+kuJOu$efN(bXCg=)3M+Z*2D$tV8A+I4S|%eZ{ynE%1oPWTFO7-AXX zwJ9<_iY5)-HaX25ciqE_FTcjzPtW5^M;*@LOP8P#6M8(ulX;+r*16zry-=Ht^Ql>nRon7#-b;>sa&^ zvi#uM@3ZUdnP@6B8+8oBz_x6rTW#)t=wY6E<~csx^f9KL#dU4MxW!RNe4e9^_#%6L z`cv3|@A)W-g4Mle&yF1@w6giqs^80r`+RWF8WXNyXT2!<%gPYPBE(^Nc_1tJQMG}@_* zAr2!{() zN0_!tUul5pT8&1lEe#4u$T>D~y^58DTzmOnZl8ppCMT+_gBH*qbKFlbT8<(L>7 z!!%9#dsS5lfp)Y;>@w zo9T+blLP9`4(xDIqdeUpQ|#yYH#TtXjW-h;8ERfgs_7_7%HTkW?OV6txHcKbW_)ZE z+texMbHs5bkxBLn=)>Z>U^DkK3Y(M~DU zE8(3S5JC6SR2&H9$uh$OefVAj+cAm#Hku_cL8I0VNOYHHUwwmH?!1TXl?H>ec46D- z7>;8RM*+kUnxzwO*IUY&jX8 z_{&TD=%+uY()3ZRELATco68|kT2)dsftvz(g0?i)Ff>^QzVCOyKv5+$_F!5#AgWEb zGqf5t6whPfr)KljWyf&L;h#fv5wBGzPD3)83_%j3t0qAhVd^$ow>0DlR2yyjvLf0r zRj!cF=P~t8^J#@CPd@!;?*GkWe6V>lZgBwJ%HRcoe0{4?#ZZuWF7iE$B0SF{NfLBj zn4!u?RW>$AVu+F$*?klR!*yxa8)P&UHT0NS%(LQ@lUe%t17z*?K)voa$XQwWvzF-a zrj0IwGSLdstRwOYUVP;Z);#b4j^nam;X?LXbRgA6gYRE=J);v7%wMoKW0O-fnjVEx zAJbE1c9}6mt5KJEt{J*?Le)B-rwGVwH6?V4B2J`5DjnojBpM2O5rqK*`8;8($<($j z?6>dU9Dm$0X3ier*%zMWnLqyt&ug;B?z?f-w0du@g{P` zK1L@dvE3|fKOkEyQK^)Pk`&9aiNc6545XnM%Rmi%(kSXiRY4U|S2a98pw((KG(3YK z3h~3ZlgXMPoe`}jOd^3lsA+@mDk#+`O*IYfz3%~j`JeY==ZZ|%Jv7U~b~9|+yqTG^XOYASf!`#T&4}hey*iC! z=%no?g^Y`3IT)tJ$gJ7ywf8>EnzJjW<#5{_chK^DT5Vs#sBl2hRFXI)5`~_$Wy*T> z27wJF}s7!ZuoGVzdZZA z?7n^!6NE8wn#fL?s)|gbG!^}(O+K5Y-41EgTR2V*(-Ms{MKN^h%^HPbR`wP(@QA%S zmz;Yx$1Xig0**2jrs=cKzsU7B{tJ30Po_A?#%-g_nmw1Tqhn-pE|Kq%w+)ok$FEnJ zx7%EnFFTe)mMq566)aUi9JFK5HE2cf@S{)ii(lSHoT!o)i>ONmkfsuiP*ss>67OaG zs0OC#62=e<2B8_qw;C$dS{cVsaWsWe#$qs^;kKJ@=&*-snx<#1x|q?)Dv53rq&i*@ zqZ<~Mt`d3;267e?+dg8rRN$Nyr*YWl4@MIRq6#7`sGcq%QCy7>ezoQi9(n8!pqdz# z&G^J5impkhwQNlmWM96(==d19Tme6bWWk%ZKtcsS>>!+F+i0r7QeMOWcz^m4n zH9W+3FS&%z%%6ku`0H<^Km5tNqaA$31R(@iYHCJjAYEuk?v%F=@uvEr1kvCGV1 zlqANnEeW$V&%#OuWOwq+2tJ(56t z1rmu$g*+=xJ&mPH4@GIFX?oKSf6TKly+*kafR#bjErKABFqKgg{^8OKIb{F6@EQ%t z-3ymX&mCMZCZFlO8 zhK#i3IwWBtX@%?!qAmm#5gl7RPh(p38d=+t_l-PH_KMFPu!wJ;a}Fbe*$x}sHZj3F zAAHQ?Pe0GIFTaNEDcf0lRO+sX%$b;z(S z+1Nzz;;6%|EDZ_Qm5DL-+h=dS@wJoKfA4v+ku{qEN|HvYfXfj0XtcsDx8KG48#l=a z#JI~eyF+1=BRLkM<=U?G_-(O8%v4CY5EIalXmLK;Oa^3k^6n8s< zO1;ghOD?56T_@5EO8tX0n=R7Nm+2~pBGFe=g(w!lAOr-3!1u`7CX4p{3}>Hy8gmBo zo!|9lm^2O3YQ2GzD@rGB{Lx)J@zm4u1M`l}z5jLxjt&_?W#WWJy-Kc7kZ92eVjla; z3q0`9BW&HWiDQmBiVMzJAz{!`A&3$by)#mZI=SU%_wm@1e*)dYk5idON*FLSP?CjS zu2g7v?Jlp z!dOM&4^KVEPw&2$L^T+jtTHk(jIE|Dm_HBGvUuyA4Q$=E4JVt!vJ9H_Du$+`#vz9; z-k-}Z`Zl%#T9U}q5yc}Nh?A5=HKhaRUUVg!KHkE>KtKJ39RK={*ANFWu7D&#U_`~q z4y4G&<>_$2rB}0gq1Gg*|?&Grr29UaDka%7*toWX*$*^6KlaVVefK?Yav$UUv;9 zu|$I%m&gl|LPF#MHhj31v(7wIYC~6E`CShA+=2LUBpGIhVx-)1>#aQd+)LDIb^1y} zEM9UD7p(lIqyyr*@bcAsxOp?4FGK_j!*-}vt7L48IA}3zaERr{eu;zj-;Z5}`=q!f z2C2?K(`xg?Q-9`HYaYU|Gc=n`IVBDE7y0*}{{&MMq3>Z@MklG2j>I7f(F+qyTjRV{ z7xT`Bci46At}NJVK6}iY$AOC%Gu&U|zWX2K_rHH!;(MiBW%1(0Tzv8Qa=7V8lPN1# zT|lkn;l&9tI*FhuaVi^`XkO**+ZZY3S+Zz9DZA{y&u8e%XHe>CnqGXxRczTdM!n_J zX!$6r%C56!P?_4!{rBEO!4k@WkU2#6)a6W|86rJ8Bcd0^+<4PXtX;Q`YO_Vl56I>7 zlK;=0`)Ss$U5h5vjWlNYvahh>)Ke*BL|8iUXaotTopCnfl^SL?N2;1M{fK;_NVPUC zsf1sfCT_Q|l$2RRCBE_1uX5yJ2cf*ac{IJ`(#shyS1A-r6iNe3j8Djj_hn4ZI_(sW zIpPbr5)MVODKwf*+>8*O6oN27H%va>x|K~^M|thdb-eug8?y6GOimHTDWzf_qKE_b z`z)7T@NJpLlM_=64fe}VT=(7wTz19POw}9okIY6f9Y&`rxY?W(1Vk$8BothOR(Xor zA&V9Tq(O^IE?C7E4qk*Zm8R)c z*WD~JxYce;f>tQziMyXkJg+6i4%=~wQ;3vKVT-Go zAi0#dYOR=@t6@nIz@sng$l{ffy(r-CUde{{Kf=ru7%SHZ6b(C9K%k?5R7+FQQ_=K% z62HmJ!4lV9^*wf--H%dB)AZSw*YkrLugA)`Oq9o%F*GRi{rq#!=FkHVMCj65nxYB< zCk7HRoOJP{W1~VGCkT;J5yMMY`Rhq2%~pi#>artC>D==%&CY1~!B~ZJ&cCpe^TmFq zJs&5Nr&4QTI}WB{;MJ$G#2ecUsh%u7WC`d0uN6d5j8cn*W$=yD&mmD$T7Co1tMi?U zF5s|(4noALlO&=KbS>x>Z>I_q6e&N7ajW+zBv!#PQ~4I%(ukszdZSLUFTX=75z+tQ z-M`{@PyUHOmY0SfrWm#>rITDPC#g%`wK0fEy#^Um<;u%0V$nXIk!L8`sJO|0KKNU1 z|Jm*AF@JZCI_gN4eer0S9=e>2yBw|?ZK3BRslb?2%Icyc%C0CJOO%d7(kY07KsF`$ z5n|WTJsr>Ynf3y{b-`scV-c0+h_isTz%!`WDMbK zAXL3Ht+!IdD=+`v6)ahDASWz8P9{hcwsCc_4-yojlPZFAZW28wcE2?Ei=&5q$BbYhz!441mkCkl6%gi>8l zMXG5X^-Y|F_&<_pWl*ctZqy}p4q9~<@Bdj= zo_!W0!zGkBVdF<1Ft&9IBSS;%yZ1tAj!@l2FqE_rr;_`211r?CMzfA3WO30Z#Si4( zqkI0{ZJgjiJ1+wh@hnNvRFzILbuK{^#XQ_ws)+Zz{Ki@y{LO>B^4GUXvqPvts%+HA zSt@(Xoz35$csvL0v!{gIW~)Wsb=k6IGjnFo>7-(}6lF=u6A%R6RB1LF7>{IvU;dKU-+Y6C z8FOgHc1M*+B9`p8kdu}j!=it0Nw-R9csu4CT3c^t545ew$cqnI^!q4-)CvsR03_(mtV5rjoyO%T>l-{1YppY)-sUblEm;*5=LkH?sQrf5Z=B*#ndm zEK{eykY~owFnPgg(p2XE=-75zL5LTGOqQ$6+;tA)lVvR1C5pxSOrl1WzgxHm*I)G= zG78kn+vyj3GQpL*O16xkV#qx~PYLPliDf=_jPHPBuOvhQb)sKyiFJ>GW@tS3<~r`U z`)*nEwN`-ZWF=pjo}6IeUJDr8vPB}7SR)B|^uhqmu%)s+W6my=YfXttljVwhU(T@z zn$rwq9qzv4$Mji(`}nkL73^%b7i-xkdJgpJPwt7kyUDKcpPYa&y+htcmzfDap`TEG_^K9I*jsBrwupG8e zOk&!OTz~}i8hv>eBaLWOCb;>A)$F#*ObV{mT{Cr2S|qe|K(17F0Ei;?#5*HIC;puY ztdl}enVyzYl2~y3>+QF(VdF=XMn)K$5;JQ@nQly1n7_v!43|o*Jm+kNN)GjUD7$eG z#oTe%zp?I}ciA#NNv2Sw9Y*9_2b+ZE)Hrd}=2Np~a>MGY*=5GCr0;Ri**SEs&i+U^ zAyVd(6WEbLpTMF3z23U1?X|IOSJv?B>(;a8zaHhycQ;V=J=yj(#iE$+qdGN78V1~X z+fSH1W4N=m5gXBN0AE?Vf$v^TFM1EwZjlD2E?i{(Z$y@4A;~UU-qw@iLW0gSm5Nvv{9{ELpTK z3l=P3rr7w)hM5orAz4@a(a8UM^<3&W5PkC3Dm&<+3|q$m82~>3p&Tm^t4Pk@OcRMD zFFpSfqno!-$U2;K{Bkm;Cg)gz9ir*=MtZZF;8@c#i| WZgh_=ftl0*0000Py1Pf0{URA@tOng_fcWx4MEQ`XF^zI%2m!59d^-~j>&0^vY{(gdQyLB#_>q?kjK zq96j7TtpGe<#4IO1@S5xAPNTyARy9}5?T_GkiN6~UUmA+{k`8>JH&hM?BC9AYu3E= zd7t?)Oj9VVI{53TtvPyhJ^Hg4XE<>blb9m1%=XOq?Ro&9E38uW-RPuAu6MOf)>Y z`lfYyYE|qcVAZ!SXUU?4(rFO;EwoVOG1j9Nxt{-a(@#h;Syf7@VIn^E_H;9_V;ecgCZBT{9vr~7Ogc&> zk~qfmJfb+EtEZdMu?adlI&nRpiCP`Q$*I5uAz>V&>jq{fgCUVmQi3EUH8QH_oH*pOM=jw$R-8&+ z5Ueszv7Br5Bnhd?lR*$B7>1>u6^nmfnx=xeg7M;rw2B|N>cN#-O{Lk+=6UijuW-wq z_b}E7(49P$rca?*LJ;221dQbFlrRX0q7W-%;y5;e=aWhiO8`|l%BwWuBvq!FPL8HL z$7{0h$7XTbXOH9frHhd%;x=oq-FrBVsY zYz?1gnDW%K&+*W2|BDZ{ZYN*v#;|j^fi!!HTdN@8BXw?B7GV_Ox-LnQV47lvhGCj2 z@(TUL@HkNeF4YYzC#&W~-q0~ZpQ#-sR-AqsOOH5688`{*HNQ^LE~wu%Wx`tyrlz9} zH-og0mAZnL|N15mJ^V0EHpf2u?#lrS52jMDbNvlBGB7;C-urxl!I4pFjV2vk-HeZp zFl%ZbZoRJR+{~C{EL%|qg&LYpz0m|+Q{^+sSrYY)rqEC`4dNi6r&J$=}|N0*riMPx8=(zST>f# zs1l{A1nhxdKFlwE^)ODM#6;c2u(QgEO+GWU#C(VT|Hf znrV_~pl1v!l`*oGPS()q%Gval3f%U?8(Zw5lBVf77hcN1NQK072vQR_h%qx3mZ1}S zb-IcUqdPV+rK`kwXPwEBM;wNsX=HRE!jdOb2EV%MAw0PHQ67E#33)oE?JzVvf^HfZ zhK6lt@Vo})a*4sgK?=n(z95>W<2YFbv3?kmlHxcHx~?%gJV;Mx87+=*s}-jA^>O9p zm+^^tGtr)S{mt~2TW=@SEhd^Fbw5QnTaZZTk=0}BqXR5G^dQbU{WNAxn}U|aWF1>I zWz}`bqBdm`zZ_gQ=d&UAx~7 z&NzdmM=e2Xq-lEd|G1UsUwNIeh7VR=WgrLw1yeaa;p!_c;qU|JE4-IR&IoL!DT!tf zps{nJ!sEaDJ&!&1I77pubaZxe*kMPIp!4S1w-oz~l7!LmarT@uhibh}B*tZP#GypI zwYh1Au4afBuULMX(5zL-XKf0OMdZ1JjRuPkTF8H#e?I-ag%%s#F+9w=54P~cvoG-c zD{o-u3Tl&TRwr1p_&_eZ;Olf|HN0k%VooM@%E<6AUA_Hyz^(V(&uf2sjU797&`~P0 zeED)t{oD!c9I4aUU10tD+u68z3%B3>Q?Q1$(PO)@TH1NgAaw1|jg_z&N+uaW5Zi+M*&5`z}{q zehCW~%%^0x7VgP4JMaT^JIfO<{*|k*UqxrRtSI$~#~;t~lTV`P2#rvz9xsCNn$Ja- zUd~9hMxvW^cK6X}G)Y5W?a;yzbfF$X(xk4WI}BY#T^62&pIpGXE6!kMZ%NS!nF9)i z(lksfwK~~ihjQYkTkhq_r=L|1FXpoR{N6jsnvfTJ;)HswLa|&{RG=QjJpPxLc<48e zvSaHOjz8uzTzvjn3I=T*f+#^#?6k!x|MQ+-^7xa_fNA5$!ii$E(5J7b3)8k48=s)z zHdQ7BM1u=_@H{N2( zlqnSRS#JB`^&}#NXt9cjL?(?B)qs(%G3dc5Upk$xQh{Yh9>E!>f00~!-;j(+bGu@! zrt!z8U*OLB9w5;z21mx3+TV|@C+stCFDyIDJL}f5ZN~ulLLSSusLPfnJ9QkgWYIx< z^BXI1g!v|kLM4)<<$z2`c~f!V>)*JV&0Dt7)6-3Nxx`Pte=Ttkle5ICMBRkm+TUbw zxh7nE#kFkRw2`@UX7l44S1BlT6rqD=;A0ClcSC|yXT>=ysWd!_hNu!6j0Nz&fBi6P9$C%w{wdtB>N@(n zJMjabtks$oa=9`5?ukEf>m5I(SngtYbb@6|mva8sR?t<{_|a`YxyUUNBk(ITdFck#=I*6@cv{85p(@v#XOEn3W_mtIIBLrh7N zX^TuYJ=`cE79~$NRg*im1yRV@;0~sAmig3y`*Y-Bhj8G2`_fq|pw-eez3i%M*g7yo zt>IC3eRNG{*7T`Nj102+=l4;zML7_eBT~dH3L}h+lt)WRa>JOLZoZkdZ@)ve(V*%3 z3aMl`?J;{#-g@h8Oxga^l;tO$$XREcK{+pBX~m-+B%FEnxeSe0u<|8Rk!;j1(n)n< zLdi5(&+QfwOaMuS{lgeQ$43>5L(vVA+7w-4~z+P8W2^*0F9l;P1)rABmibdW|N z2On?%-@JIGs^gL2QTlqj)jPiP{s&y~?Q0mVHt6b~fo5mfIWkVJP*Rk}G7L;&$~l|H z*a)?eAy%GqHm836b7&i?UV7n0mnaol)Uq8rhsfu$IHu0n&TX7>;<23l?_Z?5kVnZb zQukqqnaQ>rRm{=w71uZL#kji9`yXy*iP`|Qnszv?p8fa9ZMN{587?9g!vk=AN# zxf%(IOUYGjCKPizw!muLrL&M#&8sAP)xcM?{vRJIv25pfRf%XaSWU~p_x)A|(ilDT zN&N=Xdb?P4?bXbl-h)<6)AadQ-{reEeGey}WoUGe{wcj`cwF@L^H_55!H6zxq$#=( zxHjo(k=@of7l&jG6p>QXW#|28iMY{>$mPtI3?e<_`bx1A^7g^tIOkt<2@OA_*x5t1 z5va9xyej%sR>?DNt%{?iBz}Xgyv@?X58=Y|&LWCpv}!Dt!B=LX+3(-hYQOi=Iz*iGdO+|UjL~A+J7O|orz!WHy zw~(`rc6MCfXWR`q`{FBT#sUVBq9;NrJkM9VLoO@Yk1)a(wU?!S!9H`j_S;t~W*`8f zrE!|pnki&7uDtq67Jq6HUs!&!Dvc=g$eD5;Bov}mDr%8cUp2+)We46ZHSG*c7E8Ma zDuQrj75Rop>sbdk40zzzk8tnm$J8D!Vwfy4qJGE|J~4kDQ+j*Yu??(!En1o7Q(52Lj z7KN5f$iD)CqR3UNRi#J^BlW^iMfM+?HgVb2*D>mw6ubLq)@w@D4w`iiK5&0lp8GYX zP3ck$c=N^&lz2R~uaAFOu&)9P$&3U;OY3o}xNkeKq6E|%HEhemmN6;6Q0E@){I^fz zga_?@7$|CkIBMuRtzv3jLKI~&pFC8NdtQC>Ego63hF90V3;8~DJBL@VlDAFf&Yr=S zPFc>P{aQSU&+g0|w&&XqDCkft?-BV}LNG!Y{#z;aI zAFzN^PCSl-_WhUwj6d9i zq$;6yO&fyss5P?Y#?(EJ>uQ#|#| zGpdXVt59`hgi?1ep%;?L7Ss%qa#ZCi zP24J3k-Jl+#JAg3#a}*ZwFZTv+$dAlBUf-(`;Semy7BuedNKoKM#xGjia~#0ALU|^ zG)bw}s_fV~h#LgBLCElUh3T_qF*GuYmCX@Fnz}EnSJ;2wx!ico<>WP}j_ssd&dG!; zEBJ07aA=}(iyqbfwK^wO_1rSP1&)&~AqCWmeyu6TJsFIQ!3%5O=C1qhV_aO57ai`u z^Hw?>6B8e=I*wB)OvYO2N3_2r*+&-IZ8~;%T#Ppg>K|4oAc2wY%ci!dpd+*_` z4IAj2Hj^Z=6fKrRigG^3w#}QEGkXuEb?^V; z?e!bjHaJYa+=&-PN;Jr(3AM2yrB2MA+0XZ`zm8c`rzrX!2Ofqx@t^N7^<$5*cEdUUAybqX?j`lsPVF@rm{lh+eEayB!j_4Apdmva2kN2zCvB4Mc$p7wd_ zE++Mn@&8v7@5UB(?Z7Evqe&*0CkbQxG(mSPHf-I>Bfo!~wQs*mv*rK(Egv~ zs!K0w<9CxnE=Q=AACn-3yYBrN&%XF710$o1*XqokIfF&}&1dn#{h2>+9@D1uC^jqs zjDnCtPX5!#|9efk)N(*hnkQjrk^!i&33+Q1I6nYK^h=0!6z6ZG2}7e}yz;`!3~b#- zxsd17&z(%(GSnU`=n?xqhAkvor{46axE{Y=^BAL*8XY~oeD36v=qWl%HlMuIDNlRs zxNbl$YyFcGyPMc@K<2>Y{ku~bd8geS5JL=&rZ^+VuB6&yE(Oc7TU9Pm&5-DiQ0gWOk-BA P00000NkvXXu0mjfSZmON literal 0 HcmV?d00001 diff --git a/examples/Udap.Auth.Server/Pages/Shared/_Nav.cshtml b/examples/Udap.Auth.Server/Pages/Shared/_Nav.cshtml index c43dd396..9a52d441 100644 --- a/examples/Udap.Auth.Server/Pages/Shared/_Nav.cshtml +++ b/examples/Udap.Auth.Server/Pages/Shared/_Nav.cshtml @@ -11,10 +11,14 @@ 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 b141616e..f2fb04ca 100644 --- a/examples/Udap.Identity.Provider/Udap.Identity.Provider.csproj +++ b/examples/Udap.Identity.Provider/Udap.Identity.Provider.csproj @@ -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/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/Services/FhirService.cs b/examples/clients/UdapEd/Client/Services/FhirService.cs index 0c456f36..70d927f0 100644 --- a/examples/clients/UdapEd/Client/Services/FhirService.cs +++ b/examples/clients/UdapEd/Client/Services/FhirService.cs @@ -13,7 +13,6 @@ using System.Text; using System.Text.Json; using Hl7.Fhir.Model; -using Hl7.Fhir.Rest; using Hl7.Fhir.Serialization; using UdapEd.Shared.Model; @@ -42,6 +41,13 @@ public async Task>> SearchPatient(PatientSearchMod } var bundle = new FhirJsonParser().Parse(result); + var operationOutcome = bundle.Entry.Select(e => e.Resource as OperationOutcome).ToList(); + + if (operationOutcome.Any()) + { + 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/Server/Controllers/RegisterController.cs b/examples/clients/UdapEd/Server/Controllers/RegisterController.cs index 7ea56a01..31da90f5 100644 --- a/examples/clients/UdapEd/Server/Controllers/RegisterController.cs +++ b/examples/clients/UdapEd/Server/Controllers/RegisterController.cs @@ -441,7 +441,7 @@ public async Task Register([FromBody] RegistrationRequest request DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }), new MediaTypeHeaderValue("application/json")); - + //TODO: Centralize all registration in UdapClient. See RegisterTieredClient var response = await _httpClient.PostAsync(request.RegistrationEndpoint, content); if (!response.IsSuccessStatusCode) 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", From 8c371883e8dff12f6948cb7e2add13f9cbd074f2 Mon Sep 17 00:00:00 2001 From: Joseph Shook Date: Thu, 19 Oct 2023 15:32:19 -0700 Subject: [PATCH 19/42] Dynamic Tiered Provider wip Retain some work. Time to clean up code after this. The dynamic parts are working and close. Still may switch from inheriting from OAuthHandler to OpenIdHandler before this push is done. --- .../Certificates/IssuedCertificateStore.cs | 3 +- Udap.Common/Metadata/Community.cs | 5 - Udap.Common/Models/IssuedCertificate.cs | 1 - Udap.Common/Models/TieredClient.cs | 2 + .../DependencyInjection/Additional.cs | 20 ++ .../UdapBuilderExtensions/UdapCore.cs | 6 +- Udap.Server/Configuration/ServerSettings.cs | 9 +- Udap.Server/Entities/TieredClient.cs | 2 + .../Oidc/IdentityServerBuilderExtensions.cs | 28 +++ .../Oidc/UdapOidcConfigureOptions.cs | 15 +- .../Oidc/UdapServerBuilderOidcExtensions.cs | 20 +- .../UdapInMemoryIdentityProviderStore.cs | 4 +- Udap.Server/Models/UdapIdentityProvider.cs | 44 ++++ .../TieredOAuthAuthenticationHandler.cs | 39 ++-- .../Stores/UdapIdentityProviderStore.cs | 112 ++++++++++ .../UdapServer.Tests/Common/TestExtensions.cs | 9 +- .../Common/UdapAuthServerPipeline.cs | 12 +- .../Conformance/Tiered/TieredOauthTests.cs | 22 +- _tests/UdapServer.Tests/appsettings.Auth.json | 2 - .../Properties/launchSettings.json | 2 +- .../Udap.Auth.Server/HostingExtensions.cs | 195 +++++++++--------- .../Pages/Account/Login/Index.cshtml | 2 +- .../Pages/Account/Login/Index.cshtml.cs | 34 +-- .../Pages/UdapAccount/Login/Index.cshtml | 28 ++- .../Pages/UdapAccount/Login/Index.cshtml.cs | 69 +------ .../Pages/UdapAccount/Login/ViewModel.cs | 4 +- .../UdapDb.SqlServer/SeedData.Auth.Server.cs | 25 ++- .../UdapDb.SqlServer/Seed_GCP_Auth_Server.cs | 47 +++-- ...19222837_InitialSqlServerUdap.Designer.cs} | 8 +- ...=> 20231019222837_InitialSqlServerUdap.cs} | 3 +- .../UdapDb/UdapDbContextModelSnapshot.cs | 6 +- .../Migrations/SqlServer/udapSqlServerDb.sql | 3 +- 32 files changed, 478 insertions(+), 303 deletions(-) create mode 100644 Udap.Server/Hosting/DynamicProviders/Oidc/IdentityServerBuilderExtensions.cs create mode 100644 Udap.Server/Models/UdapIdentityProvider.cs create mode 100644 Udap.Server/Stores/UdapIdentityProviderStore.cs rename migrations/UdapDb.SqlServer/Udap/Server/Migrations/SqlServer/UdapDb/{20230826205429_InitialSqlServerUdap.Designer.cs => 20231019222837_InitialSqlServerUdap.Designer.cs} (99%) rename migrations/UdapDb.SqlServer/Udap/Server/Migrations/SqlServer/UdapDb/{20230826205429_InitialSqlServerUdap.cs => 20231019222837_InitialSqlServerUdap.cs} (98%) 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.Server/Configuration/DependencyInjection/Additional.cs b/Udap.Server/Configuration/DependencyInjection/Additional.cs index 6ad3f0e9..f46a904d 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; @@ -53,5 +55,23 @@ public static IUdapServiceBuilder AddUdapClientRegistrationStore(this IUdapSe return builder; } + + /// + /// Adds the identity provider store cache. + /// The TryAddTransient(typeof(T)) call allows to override the Duende IIdentityProviderStore. + /// + /// Without overriding the default IIdentityProviderStore we will not find the provider type + /// of "udap_oidc" in the IdentityProvider database table. + /// + /// The builder. + /// + public static IUdapServiceBuilder AddIdentityProviderStore(this IUdapServiceBuilder builder) + where T : IIdentityProviderStore + { + builder.Services.TryAddTransient(typeof(T)); + + return builder; + } + } diff --git a/Udap.Server/Configuration/DependencyInjection/UdapBuilderExtensions/UdapCore.cs b/Udap.Server/Configuration/DependencyInjection/UdapBuilderExtensions/UdapCore.cs index 606e678b..80673fed 100644 --- a/Udap.Server/Configuration/DependencyInjection/UdapBuilderExtensions/UdapCore.cs +++ b/Udap.Server/Configuration/DependencyInjection/UdapBuilderExtensions/UdapCore.cs @@ -124,8 +124,4 @@ public static IUdapServiceBuilder AddPrivateFileStore(this IUdapServiceBuilder b return builder; } -} - -public class OidcProvUdapProviderider -{ -} +} \ 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/Oidc/UdapOidcConfigureOptions.cs b/Udap.Server/Hosting/DynamicProviders/Oidc/UdapOidcConfigureOptions.cs index 96d129ba..d8520144 100644 --- a/Udap.Server/Hosting/DynamicProviders/Oidc/UdapOidcConfigureOptions.cs +++ b/Udap.Server/Hosting/DynamicProviders/Oidc/UdapOidcConfigureOptions.cs @@ -12,10 +12,11 @@ using IdentityModel; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; +using Udap.Server.Models; using Udap.Server.Security.Authentication.TieredOAuth; namespace Udap.Server.Hosting.DynamicProviders.Oidc; -public class UdapOidcConfigureOptions : ConfigureAuthenticationOptions +public class UdapOidcConfigureOptions : ConfigureAuthenticationOptions { /// /// Allows for configuring the handler options from the identity provider configuration. @@ -25,7 +26,7 @@ public UdapOidcConfigureOptions(IHttpContextAccessor httpContextAccessor, ILogge { } - protected override void Configure(ConfigureAuthenticationContext context) + protected override void Configure(ConfigureAuthenticationContext context) { context.AuthenticationOptions.SignInScheme = context.DynamicProviderOptions.SignInScheme; // context.AuthenticationOptions.SignOutScheme = context.DynamicProviderOptions.SignOutScheme; @@ -51,11 +52,11 @@ protected override void Configure(ConfigureAuthenticationContext(options => { // this associates the TieredOAuthAuthenticationHandler and options (TieredOAuthAuthenticationOptions) classes - // to the idp class (OidcProvider) and type value ("udap_oidc") from the identity provider store - options.DynamicProviders.AddProviderType("udap_oidc"); + // to the idp class (UdapIdentityProvider) and type value ("udap_oidc") from the identity provider store + options.DynamicProviders.AddProviderType("udap_oidc"); }); - - - - // this registers the OidcConfigureOptions to build the TieredOAuthAuthenticationOptions from the OidcProvider data + // this registers the OidcConfigureOptions to build the TieredOAuthAuthenticationOptions from the UdapIdentityProvider data builder.Services.AddSingleton, UdapOidcConfigureOptions>(); // this services from ASP.NET Core and are added manually since we're not using the // AddOpenIdConnect helper that we'd normally use statically on the AddAuthentication. builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton, TieredOAuthPostConfigureOptions>()); builder.Services.TryAddTransient(); - + // builder.Services.TryAddSingleton, TieredOAuthPostConfigureOptions>(); @@ -53,9 +51,7 @@ public static IUdapServiceBuilder AddTieredOAuthDynamicProvider(this IUdapServic builder.Services.AddHttpClient().AddHttpMessageHandler(); builder.Services.TryAddSingleton(); - builder.Services.TryAddSingleton(); - - + builder.Services.TryAddSingleton(sp => { var handler = new UdapClientMessageHandler( diff --git a/Udap.Server/Hosting/DynamicProviders/Store/UdapInMemoryIdentityProviderStore.cs b/Udap.Server/Hosting/DynamicProviders/Store/UdapInMemoryIdentityProviderStore.cs index 63585186..cb34edc5 100644 --- a/Udap.Server/Hosting/DynamicProviders/Store/UdapInMemoryIdentityProviderStore.cs +++ b/Udap.Server/Hosting/DynamicProviders/Store/UdapInMemoryIdentityProviderStore.cs @@ -23,7 +23,7 @@ public UdapInMemoryIdentityProviderStore(IEnumerable providers public Task> GetAllSchemeNamesAsync() { - using var activity = Tracing.StoreActivitySource.StartActivity("InMemoryOidcProviderStore.GetAllSchemeNames"); + using var activity = Tracing.StoreActivitySource.StartActivity("UdapInMemoryIdentityProviderStore.GetAllSchemeNames"); var items = _providers.Select(x => new IdentityProviderName { @@ -37,7 +37,7 @@ public Task> GetAllSchemeNamesAsync() public Task GetBySchemeAsync(string scheme) { - using var activity = Tracing.StoreActivitySource.StartActivity("InMemoryOidcProviderStore.GetByScheme"); + using var activity = Tracing.StoreActivitySource.StartActivity("UdapInMemoryIdentityProviderStore.GetByScheme"); var item = _providers.FirstOrDefault(x => x.Scheme == scheme); return Task.FromResult(item); diff --git a/Udap.Server/Models/UdapIdentityProvider.cs b/Udap.Server/Models/UdapIdentityProvider.cs new file mode 100644 index 00000000..c967d4fe --- /dev/null +++ b/Udap.Server/Models/UdapIdentityProvider.cs @@ -0,0 +1,44 @@ +#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; + +namespace Udap.Server.Models; +public class UdapIdentityProvider : IdentityProvider +{ + public UdapIdentityProvider() : base("udap_oidc"){} + + /// + /// Ctor + /// + public UdapIdentityProvider(IdentityProvider other) : base("udap_oidc", other) + { + } + + /// The base address of the OIDC provider. + public string? Authority { get; set; } + /// The response type. Defaults to "id_token". + public string ResponseType { get; set; } + /// The client id. + public string? ClientId { get; set; } + /// + /// The client secret. By default this is the plaintext client secret and great consideration should be taken if this value is to be stored as plaintext in the store. + /// + public string? ClientSecret { get; set; } + /// Space separated list of scope values. + public string Scope { get; set; } + /// + /// Indicates if userinfo endpoint is to be contacted. Defaults to true. + /// + public bool GetClaimsFromUserInfoEndpoint { get; set; } + /// Indicates if PKCE should be used. Defaults to true. + public bool UsePkce { get; set; } + /// Parses the scope into a collection. + public IEnumerable Scopes { get; } +} diff --git a/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthAuthenticationHandler.cs b/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthAuthenticationHandler.cs index dc962ddc..3fdcd748 100644 --- a/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthAuthenticationHandler.cs +++ b/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthAuthenticationHandler.cs @@ -16,16 +16,12 @@ 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.Authentication.OpenIdConnect; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.WebUtilities; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; @@ -33,14 +29,11 @@ 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; @@ -62,8 +55,7 @@ public TieredOAuthAuthenticationHandler( ISystemClock clock, IUdapClient udapClient, IPrivateCertificateStore certificateStore, - IUdapClientRegistrationStore udapClientRegistrationStore, - IEnumerable identityProviders + IUdapClientRegistrationStore udapClientRegistrationStore ) : base(options, logger, encoder, clock) { @@ -260,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. @@ -275,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)) @@ -307,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) { @@ -328,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; @@ -425,16 +420,15 @@ protected override async Task ExchangeCodeAsync([NotNull] OA var requestParams = Context.Request.Query; var code = requestParams["code"]; - var idpClient = await _udapClientRegistrationStore.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, - + tieredClientId, + tieredClient.TokenEndpoint, communityParam == null ? _certificateStore.IssuedCertificates.First().Certificate @@ -580,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 _udapClientRegistrationStore.UpsertTieredClient(tieredClient, Context.RequestAborted); } + + properties.SetString("client_id", idpClientId); await base.HandleChallengeAsync(properties); diff --git a/Udap.Server/Stores/UdapIdentityProviderStore.cs b/Udap.Server/Stores/UdapIdentityProviderStore.cs new file mode 100644 index 00000000..a8c5613a --- /dev/null +++ b/Udap.Server/Stores/UdapIdentityProviderStore.cs @@ -0,0 +1,112 @@ +#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.EntityFramework.Interfaces; +using Duende.IdentityServer.EntityFramework.Mappers; +using Duende.IdentityServer.Models; +using Duende.IdentityServer.Services; +using Duende.IdentityServer.Stores; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Udap.Common; +using Udap.Server.Models; + +namespace Udap.Server.Stores; +public class UdapIdentityProviderStore : IIdentityProviderStore +{ + /// + /// The DbContext. + /// + protected readonly IConfigurationDbContext Context; + + /// + /// The CancellationToken provider. + /// + protected readonly ICancellationTokenProvider CancellationTokenProvider; + + /// + /// The logger. + /// + protected readonly ILogger Logger; + + /// + /// Initializes a new instance of the class. + /// + /// The context. + /// The logger. + /// + /// context + public UdapIdentityProviderStore(IConfigurationDbContext context, ILogger logger, ICancellationTokenProvider cancellationTokenProvider) + { + Context = context ?? throw new ArgumentNullException(nameof(context)); + Logger = logger; + CancellationTokenProvider = cancellationTokenProvider; + } + + /// + public async Task> GetAllSchemeNamesAsync() + { + using var activity = Tracing.StoreActivitySource.StartActivity("UdapIdentityProviderStore.GetAllSchemeNames"); + + var query = Context.IdentityProviders.Select(x => new IdentityProviderName + { + Enabled = x.Enabled, + Scheme = x.Scheme, + DisplayName = x.DisplayName + }); + + return await query.ToArrayAsync(CancellationTokenProvider.CancellationToken); + } + + /// + public async Task GetBySchemeAsync(string scheme) + { + using var activity = Tracing.StoreActivitySource.StartActivity("UdapIdentityProviderStore.GetByScheme"); + activity?.SetTag(Tracing.Properties.Scheme, scheme); + + var idp = (await Context.IdentityProviders.AsNoTracking().Where(x => x.Scheme == scheme) + .ToArrayAsync(CancellationTokenProvider.CancellationToken)) + .SingleOrDefault(x => x.Scheme == scheme); + if (idp == null) return null; + + var result = MapIdp(idp); + if (result == null) + { + Logger.LogError("Identity provider record found in database, but mapping failed for scheme {scheme} and protocol type {protocol}", idp.Scheme, idp.Type); + } + + return result; + } + + // public async Task UpsertProviderAsync(UdapIdentityProvider provider) + // { + // using var activity = Tracing.StoreActivitySource.StartActivity("UdapIdentityProviderStore.UpsertProvider"); + // activity?.SetTag(Tracing.Properties.Scheme, provider.Authority); + // + // var idp = (await Context.IdentityProviders.AsNoTracking().Where(x => x. == scheme) + // .ToArrayAsync(CancellationTokenProvider.CancellationToken)) + // .SingleOrDefault(x => x.Scheme == scheme); + // + // } + + /// + /// Maps from the identity provider entity to identity provider model. + /// + /// + /// + protected virtual IdentityProvider MapIdp(Duende.IdentityServer.EntityFramework.Entities.IdentityProvider idp) + { + if (idp.Type == "oidc" || idp.Type == "udap_oidc") + { + return new UdapIdentityProvider(idp.ToModel()); + } + + return null; + } +} diff --git a/_tests/UdapServer.Tests/Common/TestExtensions.cs b/_tests/UdapServer.Tests/Common/TestExtensions.cs index af70b883..d1e1eda8 100644 --- a/_tests/UdapServer.Tests/Common/TestExtensions.cs +++ b/_tests/UdapServer.Tests/Common/TestExtensions.cs @@ -19,6 +19,7 @@ using Udap.Client.Configuration; using Udap.Model; using Udap.Server.Hosting.DynamicProviders.Oidc; +using Udap.Server.Models; using Udap.Server.Security.Authentication.TieredOAuth; namespace UdapServer.Tests.Common; @@ -80,8 +81,8 @@ public static IServiceCollection AddTieredOAuthDynamicProviderForTests( services.Configure(options => { // this associates the TieredOAuthAuthenticationHandler and options (TieredOAuthAuthenticationOptions) classes - // to the idp class (OidcProvider) and type value ("udap_oidc") from the identity provider store - options.DynamicProviders.AddProviderType("udap_oidc"); + // to the idp class (UdapIdentityProvider) and type value ("udap_oidc") from the identity provider store + options.DynamicProviders.AddProviderType("udap_oidc"); }); @@ -89,7 +90,7 @@ public static IServiceCollection AddTieredOAuthDynamicProviderForTests( - // this registers the OidcConfigureOptions to build the TieredOAuthAuthenticationOptions from the OidcProvider data + // this registers the OidcConfigureOptions to build the TieredOAuthAuthenticationOptions from the UdapIdentityProvider data services.AddSingleton, UdapOidcConfigureOptions>(); @@ -99,7 +100,7 @@ public static IServiceCollection AddTieredOAuthDynamicProviderForTests( - services.TryAddSingleton, TieredOAuthPostConfigureOptions>(); + //services.TryAddSingleton, TieredOAuthPostConfigureOptions>(); diff --git a/_tests/UdapServer.Tests/Common/UdapAuthServerPipeline.cs b/_tests/UdapServer.Tests/Common/UdapAuthServerPipeline.cs index 1b4c756f..8b48d35f 100644 --- a/_tests/UdapServer.Tests/Common/UdapAuthServerPipeline.cs +++ b/_tests/UdapServer.Tests/Common/UdapAuthServerPipeline.cs @@ -13,7 +13,6 @@ using System.Net; using System.Security.Claims; using System.Security.Cryptography.X509Certificates; -using System.Web; using Duende.IdentityServer; using Duende.IdentityServer.Configuration; using Duende.IdentityServer.Events; @@ -33,16 +32,15 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Microsoft.Extensions.Primitives; using Moq; using Udap.Auth.Server.Pages; using Udap.Client.Client; using Udap.Common; using Udap.Common.Certificates; using Udap.Common.Models; -using Udap.Model; using Udap.Server.Configuration.DependencyInjection; -using Udap.Server.Hosting.DynamicProviders.Store; +using Udap.Server.Hosting.DynamicProviders.Oidc; +using Udap.Server.Models; using Udap.Server.Registration; using Udap.Server.Security.Authentication.TieredOAuth; using UnitTests.Common; @@ -79,7 +77,7 @@ public class UdapAuthServerPipeline public IdentityServerOptions Options { get; set; } public List Clients { get; set; } = new List(); - public List OidcProviders { get; set; } = new List(); + public List UdapIdentityProvider { 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(); @@ -197,9 +195,7 @@ public void ConfigureServices(WebHostBuilderContext builder, IServiceCollection .AddInMemoryIdentityResources(IdentityScopes) .AddInMemoryApiResources(ApiResources) .AddTestUsers(Users) - .AddInMemoryOidcProviders(OidcProviders) - .AddInMemoryCaching() - .AddIdentityProviderStoreCache() + .AddInMemorIdentityProviders(UdapIdentityProvider) .AddDeveloperSigningCredential(persistKey: false); diff --git a/_tests/UdapServer.Tests/Conformance/Tiered/TieredOauthTests.cs b/_tests/UdapServer.Tests/Conformance/Tiered/TieredOauthTests.cs index 4ca67504..06480378 100644 --- a/_tests/UdapServer.Tests/Conformance/Tiered/TieredOauthTests.cs +++ b/_tests/UdapServer.Tests/Conformance/Tiered/TieredOauthTests.cs @@ -83,14 +83,6 @@ private void BuildUdapAuthorizationServer() s.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) @@ -143,11 +135,11 @@ private void BuildUdapAuthorizationServer() }); - var _oidcProviders = new List() + var _oidcProviders = new List() { - new OidcProvider + new UdapIdentityProvider { - Scheme = "idpserver2", + Scheme = "udap-tiered", Authority = "template", //TODO: hoping I can remove this template idea and be purely dynamic. ClientId = "client", ClientSecret = "secret", @@ -157,7 +149,7 @@ private void BuildUdapAuthorizationServer() } }; - _mockAuthorServerPipeline.OidcProviders = _oidcProviders; + _mockAuthorServerPipeline.UdapIdentityProvider = _oidcProviders; using var serviceProvider = services.BuildServiceProvider(); @@ -822,9 +814,9 @@ public async Task Tiered_OAuth_With_DynamicProvider() // response after discovery and registration _mockAuthorServerPipeline.BrowserClient.AllowCookies = true; // Need to set the idsrv cookie so calls to /authorize will succeed - _mockAuthorServerPipeline.BrowserClient.GetXsrfCookie("https://server/federation/idpserver2/signin", 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/federation/idpserver2/signin", 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(); @@ -859,7 +851,7 @@ public async Task Tiered_OAuth_With_DynamicProvider() // _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/idpserver2/signin?"); + 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(); diff --git a/_tests/UdapServer.Tests/appsettings.Auth.json b/_tests/UdapServer.Tests/appsettings.Auth.json index 71f5c34f..f3c12556 100644 --- a/_tests/UdapServer.Tests/appsettings.Auth.json +++ b/_tests/UdapServer.Tests/appsettings.Auth.json @@ -8,7 +8,6 @@ "Communities": [ { "Name": "udap://idp-community-1", - "IdPBaseUrl": "https://idpserver", "Anchors": [ { "FilePath": "CertStore/anchors/caLocalhostCert.cer" @@ -26,7 +25,6 @@ }, { "Name": "udap://idp-community-2", - "IdPBaseUrl": "https://idpserver2", "Anchors": [ { "FilePath": "CertStore/anchors/caLocalhostCert2.cer" 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/HostingExtensions.cs b/examples/Udap.Auth.Server/HostingExtensions.cs index 867c10e8..6c59161f 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; @@ -67,7 +71,6 @@ public static WebApplication ConfigureServices(this WebApplicationBuilder builde options.DefaultUserScopes = udapServerOptions.DefaultUserScopes; options.ServerSupport = udapServerOptions.ServerSupport; options.ForceStateParamOnAuthorizationCode = udapServerOptions.ForceStateParamOnAuthorizationCode; - options.IdPMappings = udapServerOptions.IdPMappings; options.LogoRequired = udapServerOptions.LogoRequired; }, // udapClientOptions => @@ -92,12 +95,13 @@ public static WebApplication ConfigureServices(this WebApplicationBuilder builde _ => throw new Exception($"Unsupported provider: {provider}") }) .AddUdapResponseGenerators() - .AddSmartV2Expander(); + .AddSmartV2Expander() + .AddTieredOAuthDynamicProvider(); + builder.Services.Configure(builder.Configuration.GetSection(Common.Constants.UDAP_FILE_STORE_MANIFEST)); - builder.Services.AddIdentityServer(options => { @@ -141,99 +145,106 @@ public static WebApplication ConfigureServices(this WebApplicationBuilder builde .AddResourceStore() .AddClientStore() //TODO remove - .AddTestUsers(TestUsers.Users); - - + .AddTestUsers(TestUsers.Users) + .AddIdentityProviderStore(); // last to register wins. Uhg! - //TODO: Hack for connectionathon for the time being - - 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 + // 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. // - .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?community=udap://Provider2"; - }) - .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"; + // .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 + // // + // .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?community=udap://Provider2"; + // }) + // .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/UdapAccount/Login/Index.cshtml b/examples/Udap.Auth.Server/Pages/UdapAccount/Login/Index.cshtml index ee064a5b..ac7cdd50 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 187b94c1..f9af8d75 100644 --- a/examples/Udap.Auth.Server/Pages/UdapAccount/Login/Index.cshtml.cs +++ b/examples/Udap.Auth.Server/Pages/UdapAccount/Login/Index.cshtml.cs @@ -10,7 +10,9 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.AspNetCore.WebUtilities; +using Serilog.Filters; using Udap.Server.Configuration; +using Udap.Server.Security.Authentication.TieredOAuth; namespace Udap.Auth.Server.Pages.UdapAccount.Login; @@ -24,6 +26,7 @@ public class Index : PageModel private readonly ServerSettings _serverSettings; private readonly IAuthenticationSchemeProvider _schemeProvider; private readonly IIdentityProviderStore _identityProviderStore; + private readonly AuthenticationService _authenticationService; public ViewModel View { get; set; } @@ -34,6 +37,7 @@ public Index( IIdentityServerInteractionService interaction, IAuthenticationSchemeProvider schemeProvider, IIdentityProviderStore identityProviderStore, + AuthenticationService authenticationService, IEventService events, ServerSettings serverSettings, TestUserStore users = null) @@ -44,6 +48,7 @@ public Index( _interaction = interaction; _schemeProvider = schemeProvider; _identityProviderStore = identityProviderStore; + _authenticationService = authenticationService; _events = events; _serverSettings = serverSettings; } @@ -191,27 +196,15 @@ private async Task BuildModelAsync(string returnUrl) } var schemes = await _schemeProvider.GetAllSchemesAsync(); - - var providers = schemes - .Where(x => x.DisplayName != null) - .Select(x => + .Where(x => x.DisplayName != null && x.HandlerType != typeof(TieredOAuthAuthenticationHandler)) + .Select(x => new ViewModel.ExternalProvider { - var (enrichedReturnUrl, matchIdp) = LoadReturnUrl(x, returnUrl); - + DisplayName = x.DisplayName ?? x.Name, + AuthenticationScheme = x.Name + }).ToList(); - var externalProvider = new ViewModel.ExternalProvider - { - DisplayName = x.DisplayName ?? x.Name, - AuthenticationScheme = x.Name, - ReturnUrl = enrichedReturnUrl, - IsChosenIdp = matchIdp - }; - - return externalProvider; - }) - .ToList(); var dynamicSchemes = (await _identityProviderStore.GetAllSchemeNamesAsync()) .Where(x => x.Enabled) @@ -221,6 +214,7 @@ private async Task BuildModelAsync(string returnUrl) DisplayName = x.DisplayName, ReturnUrl = returnUrl }); + providers.AddRange(dynamicSchemes); @@ -242,45 +236,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.ToString().StartsWith(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/ViewModel.cs b/examples/Udap.Auth.Server/Pages/UdapAccount/Login/ViewModel.cs index fa113034..6c8c44c4 100644 --- a/examples/Udap.Auth.Server/Pages/UdapAccount/Login/ViewModel.cs +++ b/examples/Udap.Auth.Server/Pages/UdapAccount/Login/ViewModel.cs @@ -7,7 +7,9 @@ public class ViewModel public IEnumerable ExternalProviders { get; set; } = Enumerable.Empty(); public IEnumerable VisibleExternalProviders => ExternalProviders.Where(x => !String.IsNullOrWhiteSpace(x.DisplayName)); + public ExternalProvider? TieredProvider => ExternalProviders.SingleOrDefault(p => p.AuthenticationScheme == "udap-tiered"); + public bool IsExternalLoginOnly => EnableLocalLogin == false && ExternalProviders?.Count() == 1; public string ExternalLoginScheme => IsExternalLoginOnly ? ExternalProviders?.SingleOrDefault()?.AuthenticationScheme : null; @@ -17,7 +19,5 @@ public class ExternalProvider public string AuthenticationScheme { get; set; } public string ReturnUrl { get; set; } - - public bool IsChosenIdp { get; set; } } } \ 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 15550856..a24be1e7 100644 --- a/migrations/UdapDb.SqlServer/SeedData.Auth.Server.cs +++ b/migrations/UdapDb.SqlServer/SeedData.Auth.Server.cs @@ -74,6 +74,28 @@ public static async Task EnsureSeedData(string connectionString, string cer var udapContext = serviceScope.ServiceProvider.GetRequiredService(); await udapContext.Database.MigrateAsync(); + + // + // Load udap dynamic auth provider + // + if (!configDbContext.IdentityProviders.Any(i => i.Scheme == "udap-tiered")) + { + await configDbContext.IdentityProviders.AddAsync( + new OidcProvider() + { + Scheme = "udap-tiered", + Authority = "template", + ClientId = "udap.auth.server", + Type = "udap_oidc", + UsePkce = false, + Scope = "openid email profile" + }.ToEntity()); + + await configDbContext.SaveChangesAsync(); + } + + + var clientRegistrationStore = serviceScope.ServiceProvider.GetRequiredService(); @@ -486,7 +508,6 @@ private static async Task SeedFhirScopes( { var apiScopes = configDbContext.ApiScopes .Include(s => s.Properties) - .Where(s => s.Enabled) .Select(s => s) .ToList(); @@ -540,7 +561,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"))) { diff --git a/migrations/UdapDb.SqlServer/Seed_GCP_Auth_Server.cs b/migrations/UdapDb.SqlServer/Seed_GCP_Auth_Server.cs index a555a671..15281000 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,31 @@ public static async Task EnsureSeedData(string connectionString, string cer var udapContext = serviceScope.ServiceProvider.GetRequiredService(); await udapContext.Database.MigrateAsync(); - var clientRegistrationStore = serviceScope.ServiceProvider.GetRequiredService(); + + // + // Load udap dynamic auth provider + // + if (!configDbContext.IdentityProviders.Any(i => i.Scheme == "udap-tiered")) + { + await configDbContext.IdentityProviders.AddAsync( + new OidcProvider() + { + Scheme = "udap-tiered", + Authority = "template", + ClientId = "udap.auth.server", + Type = "udap_oidc", + UsePkce = false, + Scope = "openid email profile" + }.ToEntity()); + + await configDbContext.SaveChangesAsync(); + } + + + var clientRegistrationStore = serviceScope.ServiceProvider.GetRequiredService(); + if (!udapContext.Communities.Any(c => c.Name == "http://localhost")) { var community = new Community { Name = "http://localhost" }; @@ -98,9 +117,6 @@ public static async Task EnsureSeedData(string connectionString, string cer } var assemblyPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); - - - // // Anchor surefhirlabs_community // @@ -300,7 +316,6 @@ private static async Task SeedFhirScopes(ConfigurationDbContext configDbContext, { var apiScopes = configDbContext.ApiScopes .Include(s => s.Properties) - .Where(s => s.Enabled) .Select(s => s) .ToList(); 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; From cc70ab8e7eef59f495506a8c8e4b2fefb69aec12 Mon Sep 17 00:00:00 2001 From: Joseph Shook Date: Fri, 20 Oct 2023 12:01:20 -0700 Subject: [PATCH 20/42] Cleaning up code. --- Udap.Model/UdapConstants.cs | 2 - .../DependencyInjection/Additional.cs | 18 --- .../Oidc/UdapServerBuilderOidcExtensions.cs | 5 +- .../Stores/UdapIdentityProviderStore.cs | 11 +- .../UdapServer.Tests/Common/TestExtensions.cs | 39 ++--- .../Common/UdapAuthServerPipeline.cs | 24 ++-- .../Conformance/Tiered/TieredOauthTests.cs | 135 ++++++++---------- .../appsettings.Development.json | 16 +-- 8 files changed, 91 insertions(+), 159 deletions(-) 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 f46a904d..fbbbb0db 100644 --- a/Udap.Server/Configuration/DependencyInjection/Additional.cs +++ b/Udap.Server/Configuration/DependencyInjection/Additional.cs @@ -55,23 +55,5 @@ public static IUdapServiceBuilder AddUdapClientRegistrationStore(this IUdapSe return builder; } - - /// - /// Adds the identity provider store cache. - /// The TryAddTransient(typeof(T)) call allows to override the Duende IIdentityProviderStore. - /// - /// Without overriding the default IIdentityProviderStore we will not find the provider type - /// of "udap_oidc" in the IdentityProvider database table. - /// - /// The builder. - /// - public static IUdapServiceBuilder AddIdentityProviderStore(this IUdapServiceBuilder builder) - where T : IIdentityProviderStore - { - builder.Services.TryAddTransient(typeof(T)); - - return builder; - } - } diff --git a/Udap.Server/Hosting/DynamicProviders/Oidc/UdapServerBuilderOidcExtensions.cs b/Udap.Server/Hosting/DynamicProviders/Oidc/UdapServerBuilderOidcExtensions.cs index c36576c7..06ebcbd6 100644 --- a/Udap.Server/Hosting/DynamicProviders/Oidc/UdapServerBuilderOidcExtensions.cs +++ b/Udap.Server/Hosting/DynamicProviders/Oidc/UdapServerBuilderOidcExtensions.cs @@ -42,10 +42,7 @@ public static IUdapServiceBuilder AddTieredOAuthDynamicProvider(this IUdapServic // AddOpenIdConnect helper that we'd normally use statically on the AddAuthentication. builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton, TieredOAuthPostConfigureOptions>()); builder.Services.TryAddTransient(); - // builder.Services.TryAddSingleton, TieredOAuthPostConfigureOptions>(); - - - + builder.Services.TryAddTransient(); builder.Services.AddHttpClient().AddHttpMessageHandler(); diff --git a/Udap.Server/Stores/UdapIdentityProviderStore.cs b/Udap.Server/Stores/UdapIdentityProviderStore.cs index a8c5613a..1110e7e1 100644 --- a/Udap.Server/Stores/UdapIdentityProviderStore.cs +++ b/Udap.Server/Stores/UdapIdentityProviderStore.cs @@ -52,7 +52,7 @@ public UdapIdentityProviderStore(IConfigurationDbContext context, ILogger public async Task> GetAllSchemeNamesAsync() { - using var activity = Tracing.StoreActivitySource.StartActivity("UdapIdentityProviderStore.GetAllSchemeNames"); + using var activity = Tracing.StoreActivitySource.StartActivity($"{nameof(UdapIdentityProviderStore)}.GetAllSchemeNames"); var query = Context.IdentityProviders.Select(x => new IdentityProviderName { @@ -65,9 +65,9 @@ public async Task> GetAllSchemeNamesAsync() } /// - public async Task GetBySchemeAsync(string scheme) + public async Task GetBySchemeAsync(string scheme) { - using var activity = Tracing.StoreActivitySource.StartActivity("UdapIdentityProviderStore.GetByScheme"); + using var activity = Tracing.StoreActivitySource.StartActivity($"{nameof(UdapIdentityProviderStore)}.GetByScheme"); activity?.SetTag(Tracing.Properties.Scheme, scheme); var idp = (await Context.IdentityProviders.AsNoTracking().Where(x => x.Scheme == scheme) @@ -76,9 +76,10 @@ public async Task GetBySchemeAsync(string scheme) if (idp == null) return null; var result = MapIdp(idp); + if (result == null) { - Logger.LogError("Identity provider record found in database, but mapping failed for scheme {scheme} and protocol type {protocol}", idp.Scheme, idp.Type); + Logger.LogWarning("Identity provider record found in database, but mapping failed for scheme {scheme} and protocol type {protocol}", idp.Scheme, idp.Type); } return result; @@ -100,7 +101,7 @@ public async Task GetBySchemeAsync(string scheme) /// /// /// - protected virtual IdentityProvider MapIdp(Duende.IdentityServer.EntityFramework.Entities.IdentityProvider idp) + protected virtual IdentityProvider? MapIdp(Duende.IdentityServer.EntityFramework.Entities.IdentityProvider idp) { if (idp.Type == "oidc" || idp.Type == "udap_oidc") { diff --git a/_tests/UdapServer.Tests/Common/TestExtensions.cs b/_tests/UdapServer.Tests/Common/TestExtensions.cs index d1e1eda8..21608d82 100644 --- a/_tests/UdapServer.Tests/Common/TestExtensions.cs +++ b/_tests/UdapServer.Tests/Common/TestExtensions.cs @@ -7,17 +7,15 @@ // */ #endregion +using System.Diagnostics; using Duende.IdentityServer.Configuration; -using Duende.IdentityServer.Models; using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Authentication.OpenIdConnect; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Udap.Client.Client; using Udap.Client.Configuration; -using Udap.Model; using Udap.Server.Hosting.DynamicProviders.Oidc; using Udap.Server.Models; using Udap.Server.Security.Authentication.TieredOAuth; @@ -31,7 +29,8 @@ 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, @@ -45,6 +44,7 @@ public static AuthenticationBuilder AddTieredOAuthForTests( if (dynamicIdp.Name == "https://idpserver") { + Debug.Assert(pipelineIdp1.BackChannelClient != null, "pipelineIdp1.BackChannelClient != null"); return new UdapClient( pipelineIdp1.BackChannelClient, sp.GetRequiredService(), @@ -52,8 +52,9 @@ public static AuthenticationBuilder AddTieredOAuthForTests( sp.GetRequiredService>()); } - if (dynamicIdp?.Name == "https://idpserver2") + if (dynamicIdp.Name == "https://idpserver2") { + Debug.Assert(pipelineIdp2.BackChannelClient != null, "pipelineIdp2.BackChannelClient != null"); return new UdapClient( pipelineIdp2.BackChannelClient, sp.GetRequiredService(), @@ -61,7 +62,8 @@ public static AuthenticationBuilder AddTieredOAuthForTests( sp.GetRequiredService>()); } - return null; + throw new ArgumentException( + "Must register a DynamicIdp in test with a Name property matching one of the UdapIdentityServerPipeline instances"); }); builder.Services.TryAddSingleton(); @@ -85,28 +87,10 @@ public static IServiceCollection AddTieredOAuthDynamicProviderForTests( options.DynamicProviders.AddProviderType("udap_oidc"); }); - - - - - // this registers the OidcConfigureOptions to build the TieredOAuthAuthenticationOptions from the UdapIdentityProvider data services.AddSingleton, UdapOidcConfigureOptions>(); - - services.TryAddEnumerable(ServiceDescriptor.Singleton, TieredOAuthPostConfigureOptions>()); - services.TryAddTransient(); - - - - //services.TryAddSingleton, TieredOAuthPostConfigureOptions>(); - - - - - - services.AddScoped(sp => { @@ -114,6 +98,7 @@ public static IServiceCollection AddTieredOAuthDynamicProviderForTests( if (dynamicIdp.Name == "https://idpserver") { + Debug.Assert(pipelineIdp1.BackChannelClient != null, "pipelineIdp1.BackChannelClient != null"); return new UdapClient( pipelineIdp1.BackChannelClient, sp.GetRequiredService(), @@ -121,8 +106,9 @@ public static IServiceCollection AddTieredOAuthDynamicProviderForTests( sp.GetRequiredService>()); } - if (dynamicIdp?.Name == "https://idpserver2") + if (dynamicIdp.Name == "https://idpserver2") { + Debug.Assert(pipelineIdp2.BackChannelClient != null, "pipelineIdp2.BackChannelClient != null"); return new UdapClient( pipelineIdp2.BackChannelClient, sp.GetRequiredService(), @@ -130,7 +116,8 @@ public static IServiceCollection AddTieredOAuthDynamicProviderForTests( sp.GetRequiredService>()); } - return null; + throw new ArgumentException( + "Must register a DynamicIdp in test with a Name property matching one of the UdapIdentityServerPipeline instances"); }); services.TryAddSingleton(); diff --git a/_tests/UdapServer.Tests/Common/UdapAuthServerPipeline.cs b/_tests/UdapServer.Tests/Common/UdapAuthServerPipeline.cs index 8b48d35f..e314a460 100644 --- a/_tests/UdapServer.Tests/Common/UdapAuthServerPipeline.cs +++ b/_tests/UdapServer.Tests/Common/UdapAuthServerPipeline.cs @@ -90,19 +90,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); @@ -125,7 +125,7 @@ public void Initialize(string basePath = null, bool enableLogging = false) if (enableLogging) { - builder.ConfigureLogging((ctx, b) => + builder.ConfigureLogging((_, b) => { b.AddConsole(c => c.LogToStandardErrorThreshold = LogLevel.Debug); b.SetMinimumLevel(LogLevel.Trace); @@ -160,7 +160,7 @@ 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; }); @@ -279,7 +279,7 @@ 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 ClaimsPrincipal? Subject { get; set; } private async Task OnRegister(HttpContext ctx) { @@ -321,7 +321,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"); } diff --git a/_tests/UdapServer.Tests/Conformance/Tiered/TieredOauthTests.cs b/_tests/UdapServer.Tests/Conformance/Tiered/TieredOauthTests.cs index 06480378..f5eb2b5d 100644 --- a/_tests/UdapServer.Tests/Conformance/Tiered/TieredOauthTests.cs +++ b/_tests/UdapServer.Tests/Conformance/Tiered/TieredOauthTests.cs @@ -7,6 +7,7 @@ // */ #endregion +using System.Diagnostics; using System.IdentityModel.Tokens.Jwt; using System.Net; using System.Net.Http.Headers; @@ -16,12 +17,10 @@ using System.Text; using System.Text.Json; using Duende.IdentityServer; -using Duende.IdentityServer.EntityFramework.Stores; using Duende.IdentityServer.Models; using Duende.IdentityServer.Stores; using Duende.IdentityServer.Test; using FluentAssertions; -using IdentityModel; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.OpenIdConnect; using Microsoft.AspNetCore.Builder; @@ -53,14 +52,14 @@ public class TieredOauthTests { private readonly ITestOutputHelper _testOutputHelper; - private UdapAuthServerPipeline _mockAuthorServerPipeline = new UdapAuthServerPipeline(); - private UdapIdentityServerPipeline _mockIdPPipeline = new UdapIdentityServerPipeline(); - private UdapIdentityServerPipeline _mockIdPPipeline2 = new UdapIdentityServerPipeline("https://idpserver2", "appsettings.Idp2.json"); + private readonly UdapAuthServerPipeline _mockAuthorServerPipeline = new(); + private readonly UdapIdentityServerPipeline _mockIdPPipeline = new(); + private readonly UdapIdentityServerPipeline _mockIdPPipeline2 = new("https://idpserver2", "appsettings.Idp2.json"); - private X509Certificate2 _community1Anchor; - private X509Certificate2 _community1IntermediateCert; - private X509Certificate2 _community2Anchor; - private X509Certificate2 _community2IntermediateCert; + private readonly X509Certificate2 _community1Anchor; + private readonly X509Certificate2 _community1IntermediateCert; + private readonly X509Certificate2 _community2Anchor; + private readonly X509Certificate2 _community2IntermediateCert; public TieredOauthTests(ITestOutputHelper testOutputHelper) { @@ -80,7 +79,7 @@ private void BuildUdapAuthorizationServer() { _mockAuthorServerPipeline.OnPostConfigureServices += s => { - s.AddSingleton(new ServerSettings + s.AddSingleton(new ServerSettings { ServerSupport = ServerSupport.Hl7SecurityIG, // DefaultUserScopes = "udap", @@ -104,7 +103,7 @@ private void BuildUdapAuthorizationServer() // 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() // @@ -121,7 +120,6 @@ private void BuildUdapAuthorizationServer() _mockIdPPipeline, _mockIdPPipeline2); // point backchannel to the IdP - ; services.AddTieredOAuthDynamicProviderForTests(_mockIdPPipeline, _mockIdPPipeline2); @@ -135,9 +133,9 @@ private void BuildUdapAuthorizationServer() }); - var _oidcProviders = new List() + var oidcProviders = new List() { - new UdapIdentityProvider + new() { Scheme = "udap-tiered", Authority = "template", //TODO: hoping I can remove this template idea and be purely dynamic. @@ -149,7 +147,7 @@ private void BuildUdapAuthorizationServer() } }; - _mockAuthorServerPipeline.UdapIdentityProvider = _oidcProviders; + _mockAuthorServerPipeline.UdapIdentityProvider = oidcProviders; using var serviceProvider = services.BuildServiceProvider(); @@ -186,7 +184,7 @@ private void BuildUdapAuthorizationServer() Enabled = true, Intermediates = new List() { - new Intermediate + new() { BeginDate = _community1IntermediateCert.NotBefore.ToUniversalTime(), EndDate = _community1IntermediateCert.NotAfter.ToUniversalTime(), @@ -219,7 +217,7 @@ private void BuildUdapAuthorizationServer() Enabled = true, Intermediates = new List() { - new Intermediate + new() { BeginDate = _community2IntermediateCert.NotBefore.ToUniversalTime(), EndDate = _community2IntermediateCert.NotAfter.ToUniversalTime(), @@ -247,7 +245,7 @@ private void BuildUdapAuthorizationServer() { SubjectId = "bob", Username = "bob", - Claims = new Claim[] + Claims = new[] { new Claim("name", "Bob Loblaw"), new Claim("email", "bob@loblaw.com"), @@ -260,9 +258,9 @@ private void BuildUdapAuthorizationServer() private void BuildUdapIdentityProvider1() { - _mockIdPPipeline.OnPostConfigureServices += s => + _mockIdPPipeline.OnPostConfigureServices += services => { - s.AddSingleton(new ServerSettings + services.AddSingleton(new ServerSettings { ServerSupport = ServerSupport.UDAP, DefaultUserScopes = "udap", @@ -270,17 +268,16 @@ private void BuildUdapIdentityProvider1() // 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); }; + _mockIdPPipeline.Initialize(enableLogging: true); + Debug.Assert(_mockIdPPipeline.BrowserClient != null, "_mockIdPPipeline.BrowserClient != null"); _mockIdPPipeline.BrowserClient.AllowAutoRedirect = false; _mockIdPPipeline.Communities.Add(new Community @@ -301,7 +298,7 @@ private void BuildUdapIdentityProvider1() Enabled = true, Intermediates = new List() { - new Intermediate + new() { BeginDate = _community1IntermediateCert.NotBefore.ToUniversalTime(), EndDate = _community1IntermediateCert.NotAfter.ToUniversalTime(), @@ -325,7 +322,7 @@ private void BuildUdapIdentityProvider1() { SubjectId = "bob", Username = "bob", - Claims = new Claim[] + Claims = new[] { new Claim("name", "Bob Loblaw"), new Claim("email", "bob@loblaw.com"), @@ -340,9 +337,9 @@ private void BuildUdapIdentityProvider1() private void BuildUdapIdentityProvider2() { - _mockIdPPipeline2.OnPostConfigureServices += s => + _mockIdPPipeline2.OnPostConfigureServices += services => { - s.AddSingleton(new ServerSettings + services.AddSingleton(new ServerSettings { ServerSupport = ServerSupport.UDAP, DefaultUserScopes = "udap", @@ -350,17 +347,17 @@ private void BuildUdapIdentityProvider2() // ForceStateParamOnAuthorizationCode = false (default) AlwaysIncludeUserClaimsInIdToken = true }); - }; - _mockIdPPipeline2.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(_mockIdPPipeline2.Clients); }; + + _mockIdPPipeline2.Initialize(enableLogging: true); + Debug.Assert(_mockIdPPipeline2.BrowserClient != null, "_mockIdPPipeline2.BrowserClient != null"); _mockIdPPipeline2.BrowserClient.AllowAutoRedirect = false; _mockIdPPipeline2.Communities.Add(new Community @@ -381,7 +378,7 @@ private void BuildUdapIdentityProvider2() Enabled = true, Intermediates = new List() { - new Intermediate + new() { BeginDate = _community2IntermediateCert.NotBefore.ToUniversalTime(), EndDate = _community2IntermediateCert.NotAfter.ToUniversalTime(), @@ -405,7 +402,7 @@ private void BuildUdapIdentityProvider2() { SubjectId = "bob", Username = "bob", - Claims = new Claim[] + Claims = new[] { new Claim("name", "Bob Loblaw"), new Claim("email", "bob@loblaw.com"), @@ -425,6 +422,9 @@ private void BuildUdapIdentityProvider2() [Fact] public async Task ClientAuthorize_IdPDiscovery_IdPRegistration_IdPAuthAccess_ClientAuthAccess_Test() { + var dynamicIdp = _mockAuthorServerPipeline.ApplicationServices.GetRequiredService(); + dynamicIdp.Name = _mockIdPPipeline.BaseUrl; + // Register client with auth server var resultDocument = await RegisterClientWithAuthServer(); _mockAuthorServerPipeline.RemoveSessionCookie(); @@ -435,9 +435,6 @@ public async Task ClientAuthorize_IdPDiscovery_IdPRegistration_IdPAuthAccess_Cli var clientId = resultDocument.ClientId!; - var dynamicIdp = _mockAuthorServerPipeline.ApplicationServices.GetRequiredService(); - dynamicIdp.Name = _mockIdPPipeline.BaseUrl; - ////////////////////// // ClientAuthorize ////////////////////// @@ -522,24 +519,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/signin-tieredoauth", 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/signin-tieredoauth", 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); @@ -561,7 +559,7 @@ public async Task ClientAuthorize_IdPDiscovery_IdPRegistration_IdPAuthAccess_Cli authorizeCallbackResult.Headers.Location.Should().NotBeNull(); authorizeCallbackResult.Headers.Location!.AbsoluteUri.Should().StartWith("https://server/signin-tieredoauth?"); - 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 @@ -610,8 +608,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 @@ -662,9 +660,7 @@ public async Task ClientAuthorize_IdPDiscovery_IdPRegistration_IdPAuthAccess_Cli 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( @@ -719,6 +715,9 @@ public async Task ClientAuthorize_IdPDiscovery_IdPRegistration_IdPAuthAccess_Cli [Fact] //(Skip = "Dynamic Tiered OAuth Provider WIP")] public async Task Tiered_OAuth_With_DynamicProvider() { + var dynamicIdp = _mockAuthorServerPipeline.ApplicationServices.GetRequiredService(); + dynamicIdp.Name = _mockIdPPipeline2.BaseUrl; + // Register client with auth server var resultDocument = await RegisterClientWithAuthServer(); _mockAuthorServerPipeline.RemoveSessionCookie(); @@ -728,9 +727,7 @@ public async Task Tiered_OAuth_With_DynamicProvider() var clientId = resultDocument.ClientId!; - var dynamicIdp = _mockAuthorServerPipeline.ApplicationServices.GetRequiredService(); - dynamicIdp.Name = _mockIdPPipeline2.BaseUrl; - + ////////////////////// // ClientAuthorize ////////////////////// @@ -814,16 +811,16 @@ public async Task Tiered_OAuth_With_DynamicProvider() // response after discovery and registration _mockAuthorServerPipeline.BrowserClient.AllowCookies = true; // Need to set the idsrv cookie so calls to /authorize will succeed - _mockAuthorServerPipeline.BrowserClient.GetXsrfCookie("https://server/federation/udap-tiered/signin", new TieredOAuthAuthenticationOptions().CorrelationCookie.Name).Should().BeNull(); + _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(); + _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); - 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(); @@ -831,8 +828,8 @@ public async Task Tiered_OAuth_With_DynamicProvider() var idpClient = _mockIdPPipeline2.Clients.Single(c => c.ClientName == "AuthServer Client"); idpClient.AlwaysIncludeUserClaimsInIdToken.Should().BeTrue(); - - var backChannelAuthResult = await _mockIdPPipeline2.BrowserClient.GetAsync(backChannelChallengeResponse.Headers.Location); + _mockIdPPipeline2.BrowserClient.Should().NotBeNull(); + var backChannelAuthResult = await _mockIdPPipeline2.BrowserClient!.GetAsync(backChannelChallengeResponse.Headers.Location); backChannelAuthResult.StatusCode.Should().Be(HttpStatusCode.Redirect, await backChannelAuthResult.Content.ReadAsStringAsync()); @@ -902,9 +899,10 @@ public async Task Tiered_OAuth_With_DynamicProvider() // // Check the IdToken in the back channel. Ensure the HL7_Identifier is in the claims // - _testOutputHelper.WriteLine(_mockIdPPipeline2.IdToken.ToString()); + // _testOutputHelper.WriteLine(_mockIdPPipeline2.IdToken.ToString()); - _mockIdPPipeline2.IdToken.Claims.Should().Contain(c => c.Type == "hl7_identifier"); + _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 @@ -955,8 +953,7 @@ public async Task Tiered_OAuth_With_DynamicProvider() 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()); @@ -1000,23 +997,17 @@ public async Task Tiered_OAuth_With_DynamicProvider() // 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? - - + } [Fact] public async Task LoadDynamicProvider() { var identityProviderStore = _mockAuthorServerPipeline.ApplicationServices.GetRequiredService(); - + var dynamicSchemes = (await identityProviderStore.GetAllSchemeNamesAsync()) .Where(x => x.Enabled) - .Select(x => new ExternalProvider - { - AuthenticationScheme = x.Scheme, - DisplayName = x.DisplayName, - ReturnUrl = "https://code_client/callback" - }); + .Select(x => x.Scheme); dynamicSchemes.Count().Should().Be(1); } @@ -1065,14 +1056,4 @@ public async Task LoadDynamicProvider() return resultDocument; } -} - -public class ExternalProvider -{ - public string DisplayName { get; set; } - public string AuthenticationScheme { get; set; } - - public string ReturnUrl { get; set; } - - public bool IsChosenIdp { get; set; } -} +} \ No newline at end of file diff --git a/examples/Udap.Auth.Server/appsettings.Development.json b/examples/Udap.Auth.Server/appsettings.Development.json index 8af44847..95d0b6b1 100644 --- a/examples/Udap.Auth.Server/appsettings.Development.json +++ b/examples/Udap.Auth.Server/appsettings.Development.json @@ -22,21 +22,7 @@ "ServerSettings": { "ServerSupport": "Hl7SecurityIG", - "LogoRequired": "true", - "IdPMappings": [ - { - "Scheme": "TieredOAuth", - "IdpBaseUrl": "https://host.docker.internal:5055" - }, - { - "Scheme": "TieredOAuthProvider2", - "IdpBaseUrl": "https://host.docker.internal:5057?community=udap://Provider2" - }, - { - "Scheme": "OktaForUDAP", - "IdpBaseUrl": "https://udap.zimt.work/oauth2/aus5wvee13EWm169M1d7" - } - ] + "LogoRequired": "true" }, "ConnectionStrings": { From 2aef773b6bb205feab1e66573951cd31226d4ea1 Mon Sep 17 00:00:00 2001 From: Joseph Shook Date: Fri, 20 Oct 2023 15:07:10 -0700 Subject: [PATCH 21/42] Dynamic Tiered OAuth. Unwind the Duende dynamic provider technique. After some time experimenting with Duende dynamic provider, I realized I did not need it for my dynamic behavior for registering with unknown UDAP enabled identity services. I just need a single entry like this: builder.Services.AddAuthentication() .AddTieredOAuth(options => { options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme; }); When the client app supplies a idp query param the TieredOAuthHandler is triggered. It auto registers and then creates an TieredClient entry in the DB. This reduced lot of code. It was a long journey to get here. May start to transform TieredOAuthHandler from a OAuthHandler implementation to a OpendIdHandler. The OpenIdHandler has more hooks for thing like federated signout. --- .../Oidc/UdapOidcConfigureOptions.cs | 79 --------- .../Oidc/UdapServerBuilderOidcExtensions.cs | 65 ------- Udap.Server/Models/UdapIdentityProvider.cs | 44 ----- .../TieredOAuthAuthenticationDefaults.cs | 8 +- .../TieredOAuthAuthenticationOptions.cs | 19 +- .../TieredOAuthPostConfigureOptions.cs | 10 +- .../Stores/UdapIdentityProviderStore.cs | 113 ------------ .../UdapServer.Tests/Common/TestExtensions.cs | 51 ------ .../Common/UdapAuthServerPipeline.cs | 4 - .../Conformance/Tiered/TieredOauthTests.cs | 58 ++---- .../Udap.Auth.Server/HostingExtensions.cs | 166 +++++------------- .../Pages/UdapAccount/Login/Index.cshtml | 6 +- .../Pages/UdapAccount/Login/Index.cshtml.cs | 49 +++--- .../Pages/UdapAccount/Login/InputModel.cs | 14 +- .../Pages/UdapAccount/Login/ViewModel.cs | 25 ++- .../UdapDb.SqlServer/SeedData.Auth.Server.cs | 21 --- .../UdapDb.SqlServer/Seed_GCP_Auth_Server.cs | 22 --- 17 files changed, 120 insertions(+), 634 deletions(-) delete mode 100644 Udap.Server/Hosting/DynamicProviders/Oidc/UdapOidcConfigureOptions.cs delete mode 100644 Udap.Server/Hosting/DynamicProviders/Oidc/UdapServerBuilderOidcExtensions.cs delete mode 100644 Udap.Server/Models/UdapIdentityProvider.cs delete mode 100644 Udap.Server/Stores/UdapIdentityProviderStore.cs diff --git a/Udap.Server/Hosting/DynamicProviders/Oidc/UdapOidcConfigureOptions.cs b/Udap.Server/Hosting/DynamicProviders/Oidc/UdapOidcConfigureOptions.cs deleted file mode 100644 index d8520144..00000000 --- a/Udap.Server/Hosting/DynamicProviders/Oidc/UdapOidcConfigureOptions.cs +++ /dev/null @@ -1,79 +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 Duende.IdentityServer.Hosting.DynamicProviders; -using Duende.IdentityServer.Models; -using IdentityModel; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; -using Udap.Server.Models; -using Udap.Server.Security.Authentication.TieredOAuth; - -namespace Udap.Server.Hosting.DynamicProviders.Oidc; -public class UdapOidcConfigureOptions : ConfigureAuthenticationOptions -{ - /// - /// Allows for configuring the handler options from the identity provider configuration. - /// - /// - public UdapOidcConfigureOptions(IHttpContextAccessor httpContextAccessor, ILogger logger) : base(httpContextAccessor, logger) - { - } - - protected override void Configure(ConfigureAuthenticationContext context) - { - context.AuthenticationOptions.SignInScheme = context.DynamicProviderOptions.SignInScheme; - // context.AuthenticationOptions.SignOutScheme = context.DynamicProviderOptions.SignOutScheme; - - // context.AuthenticationOptions.Authority = context.IdentityProvider.Authority; - - // - // When this is the first time the idp is contacted then this property will be empty until it is dynamically - // registered and placed in the Provider table. - // The razor page that calls the HttpContext.ChallengeAsync method adds the authorization endpoint - // when it request the idp servers OpenId configuration. - // - context.AuthenticationOptions.AuthorizationEndpoint = context.IdentityProvider.Authority == "template" ? string.Empty : context.IdentityProvider.Authority; - - // context.AuthenticationOptions.RequireHttpsMetadata = context.IdentityProvider.Authority.StartsWith("https"); - - context.AuthenticationOptions.ClientId = context.IdentityProvider.ClientId; - context.AuthenticationOptions.ClientSecret = context.IdentityProvider.ClientSecret; - - // context.AuthenticationOptions.ResponseType = context.IdentityProvider.ResponseType; - // context.AuthenticationOptions.ResponseMode = - // context.IdentityProvider.ResponseType.Contains("id_token") ? "form_post" : "query"; - - context.AuthenticationOptions.UsePkce = context.IdentityProvider.UsePkce; - - // context.AuthenticationOptions.Scope.Clear(); - // foreach (var scope in context.IdentityProvider.Scopes) - // { - // context.AuthenticationOptions.Scope.Add(scope); - // } - - context.AuthenticationOptions.SaveTokens = true; - // context.AuthenticationOptions.GetClaimsFromUserInfoEndpoint = context.IdentityProvider.GetClaimsFromUserInfoEndpoint; - // context.AuthenticationOptions.DisableTelemetry = true; -#if NET5_0_OR_GREATER - // context.AuthenticationOptions.MapInboundClaims = false; -#else - context.AuthenticationOptions.SecurityTokenValidator = new JwtSecurityTokenHandler - { - MapInboundClaims = false - }; -#endif - context.AuthenticationOptions.TokenValidationParameters.NameClaimType = JwtClaimTypes.Name; - context.AuthenticationOptions.TokenValidationParameters.RoleClaimType = JwtClaimTypes.Role; - - context.AuthenticationOptions.CallbackPath = context.PathPrefix + "/signin"; - // context.AuthenticationOptions.SignedOutCallbackPath = context.PathPrefix + "/signout-callback"; - // context.AuthenticationOptions.RemoteSignOutPath = context.PathPrefix + "/signout"; - } -} diff --git a/Udap.Server/Hosting/DynamicProviders/Oidc/UdapServerBuilderOidcExtensions.cs b/Udap.Server/Hosting/DynamicProviders/Oidc/UdapServerBuilderOidcExtensions.cs deleted file mode 100644 index 06ebcbd6..00000000 --- a/Udap.Server/Hosting/DynamicProviders/Oidc/UdapServerBuilderOidcExtensions.cs +++ /dev/null @@ -1,65 +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 Duende.IdentityServer.Configuration; -using Duende.IdentityServer.Stores; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Udap.Client.Client; -using Udap.Server.Models; -using Udap.Server.Security.Authentication.TieredOAuth; -using Udap.Server.Stores; - -namespace Udap.Server.Hosting.DynamicProviders.Oidc; -public static class UdapServerBuilderOidcExtensions -{ - - /// Adds the OIDC dynamic provider feature build specifically for UDAP Tiered OAuth. - /// - /// - public static IUdapServiceBuilder AddTieredOAuthDynamicProvider(this IUdapServiceBuilder builder) - { - builder.Services.Configure(options => - { - // this associates the TieredOAuthAuthenticationHandler and options (TieredOAuthAuthenticationOptions) classes - // to the idp class (UdapIdentityProvider) and type value ("udap_oidc") from the identity provider store - options.DynamicProviders.AddProviderType("udap_oidc"); - }); - - - // this registers the OidcConfigureOptions to build the TieredOAuthAuthenticationOptions from the UdapIdentityProvider data - builder.Services.AddSingleton, UdapOidcConfigureOptions>(); - - // this services from ASP.NET Core and are added manually since we're not using the - // AddOpenIdConnect helper that we'd normally use statically on the AddAuthentication. - builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton, TieredOAuthPostConfigureOptions>()); - builder.Services.TryAddTransient(); - - - builder.Services.TryAddTransient(); - builder.Services.AddHttpClient().AddHttpMessageHandler(); - - builder.Services.TryAddSingleton(); - - builder.Services.TryAddSingleton(sp => - { - var handler = new UdapClientMessageHandler( - sp.GetRequiredService(), - sp.GetRequiredService>()); - - handler.InnerHandler = sp.GetRequiredService(); - - return handler; - }); - - return builder; - } -} diff --git a/Udap.Server/Models/UdapIdentityProvider.cs b/Udap.Server/Models/UdapIdentityProvider.cs deleted file mode 100644 index c967d4fe..00000000 --- a/Udap.Server/Models/UdapIdentityProvider.cs +++ /dev/null @@ -1,44 +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 Duende.IdentityServer.Models; - -namespace Udap.Server.Models; -public class UdapIdentityProvider : IdentityProvider -{ - public UdapIdentityProvider() : base("udap_oidc"){} - - /// - /// Ctor - /// - public UdapIdentityProvider(IdentityProvider other) : base("udap_oidc", other) - { - } - - /// The base address of the OIDC provider. - public string? Authority { get; set; } - /// The response type. Defaults to "id_token". - public string ResponseType { get; set; } - /// The client id. - public string? ClientId { get; set; } - /// - /// The client secret. By default this is the plaintext client secret and great consideration should be taken if this value is to be stored as plaintext in the store. - /// - public string? ClientSecret { get; set; } - /// Space separated list of scope values. - public string Scope { get; set; } - /// - /// Indicates if userinfo endpoint is to be contacted. Defaults to true. - /// - public bool GetClaimsFromUserInfoEndpoint { get; set; } - /// Indicates if PKCE should be used. Defaults to true. - public bool UsePkce { get; set; } - /// Parses the scope into a collection. - public IEnumerable Scopes { get; } -} 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/TieredOAuthAuthenticationOptions.cs b/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthAuthenticationOptions.cs index 8a4c3866..1f53d9a2 100644 --- a/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthAuthenticationOptions.cs +++ b/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthAuthenticationOptions.cs @@ -21,28 +21,29 @@ public class TieredOAuthAuthenticationOptions : OAuthOptions public TieredOAuthAuthenticationOptions() { - CallbackPath = TieredOAuthAuthenticationDefaults.CallbackPath; - ClientId = "dynamic"; - ClientSecret = "signed metadata"; - // AuthorizationEndpoint = TieredOAuthAuthenticationDefaults.AuthorizationEndpoint; - // TokenEndpoint = TieredOAuthAuthenticationDefaults.TokenEndpoint; SignInScheme = TieredOAuthAuthenticationDefaults.AuthenticationScheme; // 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; // - // Defaults to survive the IIdentityProviderConfigurationValidator - // All of these are set during the GET /externallogin/challenge by - // placing them in the AuthenticationProperties.Parameters + // 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; } /// diff --git a/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthPostConfigureOptions.cs b/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthPostConfigureOptions.cs index a3ac9a36..12ce9429 100644 --- a/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthPostConfigureOptions.cs +++ b/Udap.Server/Security/Authentication/TieredOAuth/TieredOAuthPostConfigureOptions.cs @@ -7,12 +7,10 @@ // */ #endregion -using Microsoft.AspNetCore.Authentication.OpenIdConnect; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.DataProtection; using Microsoft.Extensions.Options; using Udap.Client.Client; -using System.Reflection.Metadata; namespace Udap.Server.Security.Authentication.TieredOAuth; @@ -26,6 +24,7 @@ public class TieredOAuthPostConfigureOptions : IPostConfigureOptions class. /// /// + /// public TieredOAuthPostConfigureOptions(UdapClientMessageHandler udapClientMessageHandler, IDataProtectionProvider dataProtection) { _udapClientMessageHandler = udapClientMessageHandler; @@ -33,7 +32,7 @@ public TieredOAuthPostConfigureOptions(UdapClientMessageHandler udapClientMessag } /// - /// Invoked to configure a instance. + /// Invoked to configure a instance. /// /// The name of the options instance being configured. /// The options instance to configured. @@ -42,15 +41,12 @@ 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, "v1"); + typeof(TieredOAuthAuthenticationHandler).FullName!, name ?? throw new ArgumentNullException(nameof(name))); options.StateDataFormat = new PropertiesDataFormat(dataProtector); } diff --git a/Udap.Server/Stores/UdapIdentityProviderStore.cs b/Udap.Server/Stores/UdapIdentityProviderStore.cs deleted file mode 100644 index 1110e7e1..00000000 --- a/Udap.Server/Stores/UdapIdentityProviderStore.cs +++ /dev/null @@ -1,113 +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 Duende.IdentityServer.EntityFramework.Interfaces; -using Duende.IdentityServer.EntityFramework.Mappers; -using Duende.IdentityServer.Models; -using Duende.IdentityServer.Services; -using Duende.IdentityServer.Stores; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; -using Udap.Common; -using Udap.Server.Models; - -namespace Udap.Server.Stores; -public class UdapIdentityProviderStore : IIdentityProviderStore -{ - /// - /// The DbContext. - /// - protected readonly IConfigurationDbContext Context; - - /// - /// The CancellationToken provider. - /// - protected readonly ICancellationTokenProvider CancellationTokenProvider; - - /// - /// The logger. - /// - protected readonly ILogger Logger; - - /// - /// Initializes a new instance of the class. - /// - /// The context. - /// The logger. - /// - /// context - public UdapIdentityProviderStore(IConfigurationDbContext context, ILogger logger, ICancellationTokenProvider cancellationTokenProvider) - { - Context = context ?? throw new ArgumentNullException(nameof(context)); - Logger = logger; - CancellationTokenProvider = cancellationTokenProvider; - } - - /// - public async Task> GetAllSchemeNamesAsync() - { - using var activity = Tracing.StoreActivitySource.StartActivity($"{nameof(UdapIdentityProviderStore)}.GetAllSchemeNames"); - - var query = Context.IdentityProviders.Select(x => new IdentityProviderName - { - Enabled = x.Enabled, - Scheme = x.Scheme, - DisplayName = x.DisplayName - }); - - return await query.ToArrayAsync(CancellationTokenProvider.CancellationToken); - } - - /// - public async Task GetBySchemeAsync(string scheme) - { - using var activity = Tracing.StoreActivitySource.StartActivity($"{nameof(UdapIdentityProviderStore)}.GetByScheme"); - activity?.SetTag(Tracing.Properties.Scheme, scheme); - - var idp = (await Context.IdentityProviders.AsNoTracking().Where(x => x.Scheme == scheme) - .ToArrayAsync(CancellationTokenProvider.CancellationToken)) - .SingleOrDefault(x => x.Scheme == scheme); - if (idp == null) return null; - - var result = MapIdp(idp); - - if (result == null) - { - Logger.LogWarning("Identity provider record found in database, but mapping failed for scheme {scheme} and protocol type {protocol}", idp.Scheme, idp.Type); - } - - return result; - } - - // public async Task UpsertProviderAsync(UdapIdentityProvider provider) - // { - // using var activity = Tracing.StoreActivitySource.StartActivity("UdapIdentityProviderStore.UpsertProvider"); - // activity?.SetTag(Tracing.Properties.Scheme, provider.Authority); - // - // var idp = (await Context.IdentityProviders.AsNoTracking().Where(x => x. == scheme) - // .ToArrayAsync(CancellationTokenProvider.CancellationToken)) - // .SingleOrDefault(x => x.Scheme == scheme); - // - // } - - /// - /// Maps from the identity provider entity to identity provider model. - /// - /// - /// - protected virtual IdentityProvider? MapIdp(Duende.IdentityServer.EntityFramework.Entities.IdentityProvider idp) - { - if (idp.Type == "oidc" || idp.Type == "udap_oidc") - { - return new UdapIdentityProvider(idp.ToModel()); - } - - return null; - } -} diff --git a/_tests/UdapServer.Tests/Common/TestExtensions.cs b/_tests/UdapServer.Tests/Common/TestExtensions.cs index 21608d82..e5833980 100644 --- a/_tests/UdapServer.Tests/Common/TestExtensions.cs +++ b/_tests/UdapServer.Tests/Common/TestExtensions.cs @@ -74,55 +74,4 @@ public static AuthenticationBuilder AddTieredOAuthForTests( TieredOAuthAuthenticationDefaults.DisplayName, configuration); } - - public static IServiceCollection AddTieredOAuthDynamicProviderForTests( - this IServiceCollection services, - UdapIdentityServerPipeline pipelineIdp1, - UdapIdentityServerPipeline pipelineIdp2) - { - services.Configure(options => - { - // this associates the TieredOAuthAuthenticationHandler and options (TieredOAuthAuthenticationOptions) classes - // to the idp class (UdapIdentityProvider) and type value ("udap_oidc") from the identity provider store - options.DynamicProviders.AddProviderType("udap_oidc"); - }); - - // this registers the OidcConfigureOptions to build the TieredOAuthAuthenticationOptions from the UdapIdentityProvider data - services.AddSingleton, UdapOidcConfigureOptions>(); - services.TryAddEnumerable(ServiceDescriptor.Singleton, TieredOAuthPostConfigureOptions>()); - services.TryAddTransient(); - - services.AddScoped(sp => - { - 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>()); - } - - throw new ArgumentException( - "Must register a DynamicIdp in test with a Name property matching one of the UdapIdentityServerPipeline instances"); - }); - - services.TryAddSingleton(); - services.TryAddSingleton(); - - return services; - } } diff --git a/_tests/UdapServer.Tests/Common/UdapAuthServerPipeline.cs b/_tests/UdapServer.Tests/Common/UdapAuthServerPipeline.cs index e314a460..13ba8174 100644 --- a/_tests/UdapServer.Tests/Common/UdapAuthServerPipeline.cs +++ b/_tests/UdapServer.Tests/Common/UdapAuthServerPipeline.cs @@ -39,8 +39,6 @@ using Udap.Common.Certificates; using Udap.Common.Models; using Udap.Server.Configuration.DependencyInjection; -using Udap.Server.Hosting.DynamicProviders.Oidc; -using Udap.Server.Models; using Udap.Server.Registration; using Udap.Server.Security.Authentication.TieredOAuth; using UnitTests.Common; @@ -77,7 +75,6 @@ public class UdapAuthServerPipeline public IdentityServerOptions Options { get; set; } public List Clients { get; set; } = new List(); - public List UdapIdentityProvider { 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(); @@ -195,7 +192,6 @@ public void ConfigureServices(WebHostBuilderContext builder, IServiceCollection .AddInMemoryIdentityResources(IdentityScopes) .AddInMemoryApiResources(ApiResources) .AddTestUsers(Users) - .AddInMemorIdentityProviders(UdapIdentityProvider) .AddDeveloperSigningCredential(persistKey: false); diff --git a/_tests/UdapServer.Tests/Conformance/Tiered/TieredOauthTests.cs b/_tests/UdapServer.Tests/Conformance/Tiered/TieredOauthTests.cs index f5eb2b5d..9ab7b1b9 100644 --- a/_tests/UdapServer.Tests/Conformance/Tiered/TieredOauthTests.cs +++ b/_tests/UdapServer.Tests/Conformance/Tiered/TieredOauthTests.cs @@ -18,7 +18,6 @@ using System.Text.Json; using Duende.IdentityServer; using Duende.IdentityServer.Models; -using Duende.IdentityServer.Stores; using Duende.IdentityServer.Test; using FluentAssertions; using Microsoft.AspNetCore.Authentication; @@ -111,17 +110,11 @@ private void BuildUdapAuthorizationServer() // 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"; - }, + { + options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme; + }, _mockIdPPipeline, - _mockIdPPipeline2); // point backchannel to the IdP - - - services.AddTieredOAuthDynamicProviderForTests(_mockIdPPipeline, _mockIdPPipeline2); + _mockIdPPipeline2); services.AddAuthorization(); // required for TieredOAuth Testing @@ -132,23 +125,6 @@ private void BuildUdapAuthorizationServer() options.BackchannelHttpHandler = _mockIdPPipeline2.Server?.CreateHandler(); }); - - var oidcProviders = new List() - { - new() - { - Scheme = "udap-tiered", - Authority = "template", //TODO: hoping I can remove this template idea and be purely dynamic. - ClientId = "client", - ClientSecret = "secret", - Type = "udap_oidc", - UsePkce = false, - Scope = "openid email profile" - } - }; - - _mockAuthorServerPipeline.UdapIdentityProvider = oidcProviders; - using var serviceProvider = services.BuildServiceProvider(); @@ -519,9 +495,9 @@ 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(); @@ -557,7 +533,7 @@ 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?"); QueryHelpers.ParseQuery(authorizeCallbackResult.Headers.Location.Query).Single(p => p.Key == "code").Value.Should().NotBeEmpty(); @@ -574,7 +550,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: @@ -590,7 +566,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); @@ -770,12 +746,12 @@ public async Task Tiered_OAuth_With_DynamicProvider() 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 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(schemes.First().Scheme); + sb.Append("scheme=").Append(TieredOAuthAuthenticationDefaults.AuthenticationScheme); sb.Append("&returnUrl=").Append(Uri.EscapeDataString(returnUrl)); clientAuthorizeUrl = sb.ToString(); @@ -1000,18 +976,6 @@ public async Task Tiered_OAuth_With_DynamicProvider() } - [Fact] - public async Task LoadDynamicProvider() - { - var identityProviderStore = _mockAuthorServerPipeline.ApplicationServices.GetRequiredService(); - - var dynamicSchemes = (await identityProviderStore.GetAllSchemeNamesAsync()) - .Where(x => x.Enabled) - .Select(x => x.Scheme); - - dynamicSchemes.Count().Should().Be(1); - } - private async Task RegisterClientWithAuthServer() { var clientCert = new X509Certificate2("CertStore/issued/fhirLabsApiClientLocalhostCert.pfx", "udap-test"); diff --git a/examples/Udap.Auth.Server/HostingExtensions.cs b/examples/Udap.Auth.Server/HostingExtensions.cs index 6c59161f..7ce51897 100644 --- a/examples/Udap.Auth.Server/HostingExtensions.cs +++ b/examples/Udap.Auth.Server/HostingExtensions.cs @@ -64,39 +64,38 @@ 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.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}") - }) + _ => throw new Exception($"Unsupported provider: {provider}") + }) .AddUdapResponseGenerators() - .AddSmartV2Expander() - .AddTieredOAuthDynamicProvider(); + .AddSmartV2Expander(); @@ -145,105 +144,24 @@ public static WebApplication ConfigureServices(this WebApplicationBuilder builde .AddResourceStore() .AddClientStore() //TODO remove - .AddTestUsers(TestUsers.Users) - .AddIdentityProviderStore(); // last to register wins. Uhg! - - // - // 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! - + .AddTestUsers(TestUsers.Users); + // .AddIdentityProviderStore(); // 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 - // // - // .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?community=udap://Provider2"; - // }) - // .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"; + // 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! + + builder.Services.AddAuthentication() + .AddTieredOAuth(options => + { + options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme; + }); builder.Services.AddSingleton(); diff --git a/examples/Udap.Auth.Server/Pages/UdapAccount/Login/Index.cshtml b/examples/Udap.Auth.Server/Pages/UdapAccount/Login/Index.cshtml index ac7cdd50..637257b6 100644 --- a/examples/Udap.Auth.Server/Pages/UdapAccount/Login/Index.cshtml +++ b/examples/Udap.Auth.Server/Pages/UdapAccount/Login/Index.cshtml @@ -91,17 +91,17 @@ {
-

Sign in with your identity provider

+

Sign in with your organizations identity provider

@{ var provider = Model.View.TieredProvider; } - - Sign-in at @provider.DisplayName + @provider.DisplayName
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 f9af8d75..f1af0e7c 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; @@ -9,10 +17,6 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; -using Microsoft.AspNetCore.WebUtilities; -using Serilog.Filters; -using Udap.Server.Configuration; -using Udap.Server.Security.Authentication.TieredOAuth; namespace Udap.Auth.Server.Pages.UdapAccount.Login; @@ -23,24 +27,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; - private readonly AuthenticationService _authenticationService; - 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, - AuthenticationService authenticationService, 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."); @@ -48,16 +48,14 @@ public Index( _interaction = interaction; _schemeProvider = schemeProvider; _identityProviderStore = identityProviderStore; - _authenticationService = authenticationService; _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 }); @@ -68,6 +66,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); @@ -108,7 +108,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 @@ -116,15 +116,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) { @@ -177,7 +177,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 @@ -185,7 +185,7 @@ private async Task BuildModelAsync(string returnUrl) EnableLocalLogin = local, }; - Input.Username = context?.LoginHint; + Input.Username = context.LoginHint ?? string.Empty; if (!local) { @@ -198,11 +198,12 @@ private async Task BuildModelAsync(string returnUrl) var schemes = await _schemeProvider.GetAllSchemesAsync(); var providers = schemes - .Where(x => x.DisplayName != null && x.HandlerType != typeof(TieredOAuthAuthenticationHandler)) + .Where(x => x.DisplayName != null) .Select(x => new ViewModel.ExternalProvider { DisplayName = x.DisplayName ?? x.Name, - AuthenticationScheme = x.Name + AuthenticationScheme = x.Name, + ReturnUrl = returnUrl }).ToList(); @@ -225,7 +226,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(); } } 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 6c8c44c4..da6000b7 100644 --- a/examples/Udap.Auth.Server/Pages/UdapAccount/Login/ViewModel.cs +++ b/examples/Udap.Auth.Server/Pages/UdapAccount/Login/ViewModel.cs @@ -1,3 +1,5 @@ +using Udap.Server.Security.Authentication.TieredOAuth; + namespace Udap.Auth.Server.Pages.UdapAccount.Login; public class ViewModel @@ -5,19 +7,26 @@ 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 ExternalProvider? TieredProvider => ExternalProviders.SingleOrDefault(p => p.AuthenticationScheme == "udap-tiered"); + 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 => + 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? DisplayName { get; set; } + public string? AuthenticationScheme { get; set; } - public string ReturnUrl { get; set; } + public string? ReturnUrl { get; set; } } } \ 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 a24be1e7..39fc440d 100644 --- a/migrations/UdapDb.SqlServer/SeedData.Auth.Server.cs +++ b/migrations/UdapDb.SqlServer/SeedData.Auth.Server.cs @@ -73,27 +73,6 @@ public static async Task EnsureSeedData(string connectionString, string cer var udapContext = serviceScope.ServiceProvider.GetRequiredService(); await udapContext.Database.MigrateAsync(); - - - // - // Load udap dynamic auth provider - // - if (!configDbContext.IdentityProviders.Any(i => i.Scheme == "udap-tiered")) - { - await configDbContext.IdentityProviders.AddAsync( - new OidcProvider() - { - Scheme = "udap-tiered", - Authority = "template", - ClientId = "udap.auth.server", - Type = "udap_oidc", - UsePkce = false, - Scope = "openid email profile" - }.ToEntity()); - - await configDbContext.SaveChangesAsync(); - } - var clientRegistrationStore = serviceScope.ServiceProvider.GetRequiredService(); diff --git a/migrations/UdapDb.SqlServer/Seed_GCP_Auth_Server.cs b/migrations/UdapDb.SqlServer/Seed_GCP_Auth_Server.cs index 15281000..618cb6d0 100644 --- a/migrations/UdapDb.SqlServer/Seed_GCP_Auth_Server.cs +++ b/migrations/UdapDb.SqlServer/Seed_GCP_Auth_Server.cs @@ -74,28 +74,6 @@ public static async Task EnsureSeedData(string connectionString, string cer await udapContext.Database.MigrateAsync(); - // - // Load udap dynamic auth provider - // - if (!configDbContext.IdentityProviders.Any(i => i.Scheme == "udap-tiered")) - { - await configDbContext.IdentityProviders.AddAsync( - new OidcProvider() - { - Scheme = "udap-tiered", - Authority = "template", - ClientId = "udap.auth.server", - Type = "udap_oidc", - UsePkce = false, - Scope = "openid email profile" - }.ToEntity()); - - await configDbContext.SaveChangesAsync(); - } - - - - var clientRegistrationStore = serviceScope.ServiceProvider.GetRequiredService(); if (!udapContext.Communities.Any(c => c.Name == "http://localhost")) From 4118a21c470786ec1c41e37425929d2992a73f62 Mon Sep 17 00:00:00 2001 From: Joseph Shook Date: Fri, 20 Oct 2023 17:04:09 -0700 Subject: [PATCH 22/42] Minor fixups --- .../appsettings.Development.json | 3 --- .../Udap.Auth.Server/appsettings.Production.json | 16 +--------------- .../UdapEd/Client/Services/FhirService.cs | 2 +- 3 files changed, 2 insertions(+), 19 deletions(-) diff --git a/examples/Udap.Auth.Server/appsettings.Development.json b/examples/Udap.Auth.Server/appsettings.Development.json index 95d0b6b1..6cec4abd 100644 --- a/examples/Udap.Auth.Server/appsettings.Development.json +++ b/examples/Udap.Auth.Server/appsettings.Development.json @@ -33,7 +33,6 @@ "Communities": [ { "Name": "udap://TieredProvider1", - "IdPBaseUrl": "https://host.docker.internal:5055", "IssuedCerts": [ { "FilePath": "CertStore/issued/fhirLabsApiClientLocalhostCert.pfx", @@ -43,7 +42,6 @@ }, { "Name": "udap://Provider2", - "IdPBaseUrl": "https://host.docker.internal:5057?community=udap://Provider2", "IssuedCerts": [ { "FilePath": "CertStore/issued/fhirLabsApiClientLocalhostCert2.pfx", @@ -53,7 +51,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/clients/UdapEd/Client/Services/FhirService.cs b/examples/clients/UdapEd/Client/Services/FhirService.cs index 70d927f0..1db5f393 100644 --- a/examples/clients/UdapEd/Client/Services/FhirService.cs +++ b/examples/clients/UdapEd/Client/Services/FhirService.cs @@ -43,7 +43,7 @@ public async Task>> SearchPatient(PatientSearchMod var bundle = new FhirJsonParser().Parse(result); var operationOutcome = bundle.Entry.Select(e => e.Resource as OperationOutcome).ToList(); - if (operationOutcome.Any()) + if (operationOutcome.Any(o => o != null)) { return new FhirResultModel>(operationOutcome.First(), response.StatusCode, response.Version); } From 1a6952238c0718f1979b398233130aac39cd1a0a Mon Sep 17 00:00:00 2001 From: Joseph Shook Date: Mon, 23 Oct 2023 08:41:23 -0700 Subject: [PATCH 23/42] Add cancellation token --- Udap.Client/Client/IUdapClient.cs | 9 +++++--- Udap.Client/Client/UdapClient.cs | 34 +++++++++++++++++++------------ 2 files changed, 27 insertions(+), 16 deletions(-) diff --git a/Udap.Client/Client/IUdapClient.cs b/Udap.Client/Client/IUdapClient.cs index b9d5bea6..55a2fdc2 100644 --- a/Udap.Client/Client/IUdapClient.cs +++ b/Udap.Client/Client/IUdapClient.cs @@ -25,13 +25,15 @@ 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; } @@ -39,7 +41,8 @@ Task ValidateResource( /// /// 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. /// /// /// diff --git a/Udap.Client/Client/UdapClient.cs b/Udap.Client/Client/UdapClient.cs index 64d0a5b0..d683d742 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 { @@ -152,7 +149,7 @@ public async Task RegisterTieredClient(st #else var content = new StringContent(JsonSerializer.Serialize(requestBody), null, "application/json"); content.Headers.ContentType!.CharSet = string.Empty; - #endif +#endif var response = await _httpClient.PostAsync(this.UdapServerMetaData?.RegistrationEndpoint, content, token); @@ -161,6 +158,12 @@ public async Task RegisterTieredClient(st var resultDocument = await response.Content.ReadFromJsonAsync(cancellationToken: token); + if (resultDocument == null) + { + resultDocument = new UdapDynamicClientRegistrationDocument(); + resultDocument.Add("error", "Unknown error"); + } + return resultDocument; } } @@ -218,12 +221,14 @@ 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); } @@ -234,12 +239,14 @@ 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); } @@ -248,7 +255,8 @@ private async Task InternalValidateResource( string baseUrl, ITrustAnchorStore? trustAnchorStore, string? community, - DiscoveryPolicy? discoveryPolicy) + DiscoveryPolicy? discoveryPolicy, + CancellationToken token = default) { baseUrl.AssertUri(); @@ -267,7 +275,7 @@ private async Task InternalValidateResource( Address = baseUrl, Community = community, Policy = _discoveryPolicy - }); + }, cancellationToken: token); if (disco.HttpStatusCode == HttpStatusCode.OK && !disco.IsError) { From d9752431aa76e98a0b881d2275eaf820277bd2f3 Mon Sep 17 00:00:00 2001 From: Joseph Shook Date: Mon, 23 Oct 2023 11:29:31 -0700 Subject: [PATCH 24/42] Logo validates on content type rather than file extension type --- Udap.Client/Client/UdapClient.cs | 8 +- .../Configuration/UdapClientOptions.cs | 3 + .../UdapDynamicClientRegistrationErrors.cs | 3 +- .../UdapDynamicClientRegistrationValidator.cs | 52 ++++++--- .../Common/UdapAuthServerPipeline.cs | 14 ++- .../RegistrationAndChangeRegistrationTests.cs | 2 +- .../Conformance/Basic/ScopeExpansionTests.cs | 2 +- .../Basic/UdapForceStateParamFalseTests.cs | 2 +- .../UdapResponseTypeResponseModeTests.cs | 16 +-- .../Conformance/Tiered/TieredOauthTests.cs | 15 ++- .../UdapServer.Tests/Hl7RegistrationTests.cs | 8 +- .../Validators/UdapDCRValidatorTests.cs | 100 ++++++++++++++---- .../appsettings.Development.json | 5 +- 13 files changed, 170 insertions(+), 60 deletions(-) diff --git a/Udap.Client/Client/UdapClient.cs b/Udap.Client/Client/UdapClient.cs index d683d742..88d39562 100644 --- a/Udap.Client/Client/UdapClient.cs +++ b/Udap.Client/Client/UdapClient.cs @@ -107,17 +107,17 @@ public async Task RegisterTieredClient(st { _logger.LogDebug($"Using certificate {clientCert.SubjectName.Name} [ {clientCert.Thumbprint} ]"); + var logoUrl = _udapClientOptions.TieredOAuthClientLogo; + 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") + .WithLogoUri(logoUrl) .WithContacts(_udapClientOptions.Contacts) - .WithTokenEndpointAuthMethod(UdapConstants.RegistrationDocumentValues - .TokenEndpointAuthMethodValue) + .WithTokenEndpointAuthMethod(UdapConstants.RegistrationDocumentValues.TokenEndpointAuthMethodValue) .WithScope(scopes) .WithResponseTypes(new List { "code" }) .WithRedirectUrls(new List { redirectUrl }) 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.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.Server/Registration/UdapDynamicClientRegistrationValidator.cs b/Udap.Server/Registration/UdapDynamicClientRegistrationValidator.cs index 2fdbbf1a..fc6b3196 100644 --- a/Udap.Server/Registration/UdapDynamicClientRegistrationValidator.cs +++ b/Udap.Server/Registration/UdapDynamicClientRegistrationValidator.cs @@ -16,6 +16,8 @@ // 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; @@ -28,6 +30,7 @@ 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; @@ -46,6 +49,7 @@ 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; @@ -57,6 +61,7 @@ public class UdapDynamicClientRegistrationValidator : IUdapDynamicClientRegistra public UdapDynamicClientRegistrationValidator( TrustChainValidator trustChainValidator, + HttpClient httpClient, IReplayCache replayCache, ServerSettings serverSettings, IHttpContextAccessor httpContextAccessor, @@ -65,6 +70,7 @@ public UdapDynamicClientRegistrationValidator( ILogger logger) { _trustChainValidator = trustChainValidator; + _httpClient = httpClient; _replayCache = replayCache; _serverSettings = serverSettings; _httpContextAccessor = httpContextAccessor; @@ -372,7 +378,8 @@ IEnumerable anchors { if (_serverSettings.LogoRequired) { - if ( ! ValidateLogoUri(document, out UdapDynamicClientRegistrationValidationResult? errorResult)) + var (successFlag, errorResult) = await ValidateLogoUri(document); + if (!successFlag) { return errorResult!; } @@ -526,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)) { @@ -537,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 { @@ -570,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/_tests/UdapServer.Tests/Common/UdapAuthServerPipeline.cs b/_tests/UdapServer.Tests/Common/UdapAuthServerPipeline.cs index 13ba8174..b421bf79 100644 --- a/_tests/UdapServer.Tests/Common/UdapAuthServerPipeline.cs +++ b/_tests/UdapServer.Tests/Common/UdapAuthServerPipeline.cs @@ -269,9 +269,21 @@ 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; } diff --git a/_tests/UdapServer.Tests/Conformance/Basic/RegistrationAndChangeRegistrationTests.cs b/_tests/UdapServer.Tests/Conformance/Basic/RegistrationAndChangeRegistrationTests.cs index 0bed08b5..4aa423c8 100644 --- a/_tests/UdapServer.Tests/Conformance/Basic/RegistrationAndChangeRegistrationTests.cs +++ b/_tests/UdapServer.Tests/Conformance/Basic/RegistrationAndChangeRegistrationTests.cs @@ -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" diff --git a/_tests/UdapServer.Tests/Conformance/Basic/ScopeExpansionTests.cs b/_tests/UdapServer.Tests/Conformance/Basic/ScopeExpansionTests.cs index 5d22b580..726277ea 100644 --- a/_tests/UdapServer.Tests/Conformance/Basic/ScopeExpansionTests.cs +++ b/_tests/UdapServer.Tests/Conformance/Basic/ScopeExpansionTests.cs @@ -523,7 +523,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" 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 9ab7b1b9..3c1215fd 100644 --- a/_tests/UdapServer.Tests/Conformance/Tiered/TieredOauthTests.cs +++ b/_tests/UdapServer.Tests/Conformance/Tiered/TieredOauthTests.cs @@ -90,7 +90,8 @@ private void BuildUdapAuthorizationServer() 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" }) ); }; @@ -249,6 +250,11 @@ private void BuildUdapIdentityProvider1() // 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); }; @@ -328,6 +334,11 @@ private void BuildUdapIdentityProvider2() // 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); }; @@ -988,7 +999,7 @@ public async Task Tiered_OAuth_With_DynamicProvider() .WithExpiration(TimeSpan.FromMinutes(5)) .WithJwtId() .WithClientName("mock tiered 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/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/Validators/UdapDCRValidatorTests.cs b/_tests/UdapServer.Tests/Validators/UdapDCRValidatorTests.cs index d7f8e10e..efd4e612 100644 --- a/_tests/UdapServer.Tests/Validators/UdapDCRValidatorTests.cs +++ b/_tests/UdapServer.Tests/Validators/UdapDCRValidatorTests.cs @@ -1,11 +1,23 @@ -using Duende.IdentityServer.Stores; +#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.Extensions.Primitives; using Microsoft.IdentityModel.Tokens; +using Microsoft.Net.Http.Headers; using Moq; +using Moq.Protected; using Udap.Common.Certificates; using Udap.Model; using Udap.Model.Registration; @@ -33,34 +45,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}"); @@ -69,10 +91,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}"); @@ -111,8 +133,25 @@ 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, @@ -160,6 +199,7 @@ public async Task ValidateJti_And_ReplayTest() private static UdapDynamicClientRegistrationDocument BuildUdapDcrValidator( + HttpClient httpClient, out UdapDynamicClientRegistrationValidator validator) { var _clock = new StubClock(); @@ -191,9 +231,10 @@ 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, @@ -202,6 +243,29 @@ private static UdapDynamicClientRegistrationDocument BuildUdapDcrValidator( 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/examples/Udap.Auth.Server/appsettings.Development.json b/examples/Udap.Auth.Server/appsettings.Development.json index 6cec4abd..cae4533b 100644 --- a/examples/Udap.Auth.Server/appsettings.Development.json +++ b/examples/Udap.Auth.Server/appsettings.Development.json @@ -17,12 +17,13 @@ "Headers": { "USER_KEY": "hobojoe", "ORG_KEY": "travelOrg" - } + }, + "TieredOAuthClientLogo": "~/udap.logo.48x48.png" }, "ServerSettings": { "ServerSupport": "Hl7SecurityIG", - "LogoRequired": "true" + "LogoRequired": "true" }, "ConnectionStrings": { From cfb84adc80e796ccc4a5e59d253c8ea9a2e0d426 Mon Sep 17 00:00:00 2001 From: Joseph Shook Date: Mon, 23 Oct 2023 11:44:49 -0700 Subject: [PATCH 25/42] Update packages --- Directory.Packages.props | 4 ++-- _tests/Directory.Packages.props | 4 ++-- examples/FhirLabsApi/FhirLabsApi.csproj | 2 +- .../clients/1_UdapClientMetadata/1_UdapClientMetadata.csproj | 2 +- .../clients/2_UdapClientMetadata/2_UdapClientMetadata.csproj | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 2dc929e5..569bf7be 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -32,7 +32,7 @@ - + @@ -44,6 +44,6 @@ - + \ No newline at end of file diff --git a/_tests/Directory.Packages.props b/_tests/Directory.Packages.props index 1367be96..52bc0e67 100644 --- a/_tests/Directory.Packages.props +++ b/_tests/Directory.Packages.props @@ -22,13 +22,13 @@ - + - + \ No newline at end of file diff --git a/examples/FhirLabsApi/FhirLabsApi.csproj b/examples/FhirLabsApi/FhirLabsApi.csproj index 2d944469..2f2ae9a4 100644 --- a/examples/FhirLabsApi/FhirLabsApi.csproj +++ b/examples/FhirLabsApi/FhirLabsApi.csproj @@ -48,7 +48,7 @@ - + diff --git a/examples/clients/1_UdapClientMetadata/1_UdapClientMetadata.csproj b/examples/clients/1_UdapClientMetadata/1_UdapClientMetadata.csproj index 0b50d648..b8aeacf6 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 86e29daa..14e00772 100644 --- a/examples/clients/2_UdapClientMetadata/2_UdapClientMetadata.csproj +++ b/examples/clients/2_UdapClientMetadata/2_UdapClientMetadata.csproj @@ -37,7 +37,7 @@ - + From 6387055becff8fa2ad43c378a915cbf4667f3cff Mon Sep 17 00:00:00 2001 From: Joseph Shook Date: Mon, 23 Oct 2023 13:28:28 -0700 Subject: [PATCH 26/42] Only show Tiered OAuth link to login when idp is supplied in query parameter. --- .../Pages/UdapAccount/Login/Index.cshtml.cs | 21 +++++++++++++++---- .../Pages/UdapAccount/Login/ViewModel.cs | 3 +++ .../appsettings.Development.json | 2 +- 3 files changed, 21 insertions(+), 5 deletions(-) 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 f1af0e7c..06448d78 100644 --- a/examples/Udap.Auth.Server/Pages/UdapAccount/Login/Index.cshtml.cs +++ b/examples/Udap.Auth.Server/Pages/UdapAccount/Login/Index.cshtml.cs @@ -17,6 +17,8 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.AspNetCore.WebUtilities; +using System.Web; namespace Udap.Auth.Server.Pages.UdapAccount.Login; @@ -199,11 +201,22 @@ private async Task BuildModelAsync(string returnUrl) var providers = schemes .Where(x => x.DisplayName != null) - .Select(x => new ViewModel.ExternalProvider + .Select(x => { - DisplayName = x.DisplayName ?? x.Name, - AuthenticationScheme = x.Name, - ReturnUrl = returnUrl + var externalProvider = new ViewModel.ExternalProvider + { + DisplayName = x.DisplayName ?? x.Name, + AuthenticationScheme = x.Name, + ReturnUrl = returnUrl + }; + + if (QueryHelpers.ParseQuery(HttpUtility.UrlDecode(returnUrl)).TryGetValue("idp", out var udapIdp)) + { + externalProvider.TieredOAuthIdp = udapIdp.FirstOrDefault(); + } + + return externalProvider; + }).ToList(); diff --git a/examples/Udap.Auth.Server/Pages/UdapAccount/Login/ViewModel.cs b/examples/Udap.Auth.Server/Pages/UdapAccount/Login/ViewModel.cs index da6000b7..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,4 @@ +using Microsoft.Extensions.Primitives; using Udap.Server.Security.Authentication.TieredOAuth; namespace Udap.Auth.Server.Pages.UdapAccount.Login; @@ -16,6 +17,7 @@ public class ViewModel public ExternalProvider? TieredProvider => ExternalProviders.SingleOrDefault(p => + !string.IsNullOrEmpty(p.TieredOAuthIdp) && p.AuthenticationScheme == TieredOAuthAuthenticationDefaults.AuthenticationScheme); @@ -28,5 +30,6 @@ public class ExternalProvider public string? AuthenticationScheme { 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/appsettings.Development.json b/examples/Udap.Auth.Server/appsettings.Development.json index cae4533b..8fc1515e 100644 --- a/examples/Udap.Auth.Server/appsettings.Development.json +++ b/examples/Udap.Auth.Server/appsettings.Development.json @@ -18,7 +18,7 @@ "USER_KEY": "hobojoe", "ORG_KEY": "travelOrg" }, - "TieredOAuthClientLogo": "~/udap.logo.48x48.png" + "TieredOAuthClientLogo": "https://host.docker.internal:5002/udap.logo.48x48.png" }, "ServerSettings": { From 4ebaa6b78eeb5525a3d821f72cd81215acf2a4a4 Mon Sep 17 00:00:00 2001 From: Joseph Shook Date: Mon, 23 Oct 2023 17:08:58 -0700 Subject: [PATCH 27/42] Building UdapClient Registration interfaces. One usage for Auth Code flow in TieredAuthTests when registering the client. Next will be client credentials flow. The client is getting close to complete. Will not use all these features in UdapEd because of the way UdapEd is meant to break down the process for diagnostics and visualization of the UDAP process. --- Udap.Client/Client/IUdapClient.cs | 38 ++- Udap.Client/Client/UdapClient.cs | 234 ++++++++++++------ .../UdapDcrBuilderForAuthorizationCode.cs | 31 +-- .../UdapDcrBuilderForClientCredentials.cs | 45 +--- .../UdapDynamicClientRegistrationDocument.cs | 66 +++-- .../SignedSoftwareStatementBuilder.cs | 7 +- .../UdapServer.Tests/Common/TestExtensions.cs | 12 +- .../Conformance/Basic/ScopeExpansionTests.cs | 128 ++++------ .../Conformance/Tiered/TieredOauthTests.cs | 76 +++--- .../Validators/UdapDCRValidatorTests.cs | 2 - .../Server/Controllers/RegisterController.cs | 27 +- ...DcrBuilderForAuthorizationCodeUnchecked.cs | 23 +- ...DcrBuilderForClientCredentialsUnchecked.cs | 22 +- 13 files changed, 338 insertions(+), 373 deletions(-) diff --git a/Udap.Client/Client/IUdapClient.cs b/Udap.Client/Client/IUdapClient.cs index 55a2fdc2..58c9aa42 100644 --- a/Udap.Client/Client/IUdapClient.cs +++ b/Udap.Client/Client/IUdapClient.cs @@ -38,11 +38,11 @@ Task ValidateResource( UdapMetadata? UdapDynamicClientRegistrationDocument { get; set; } UdapMetadata? UdapServerMetaData { get; set; } - + /// /// Register a TieredClient in the Authorization Server. /// 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 pick a different community the client can add a community query parameter to the . /// /// /// @@ -54,6 +54,40 @@ Task RegisterTieredClient(string redirect string scopes, CancellationToken token = default); + /// + /// Register a UdapClient in the Authorization Server. + /// To pick a different community the client can add a community query parameter. + /// + /// + /// + /// + /// + /// + /// + Task RegisterClientAuthCode( + IEnumerable certificates, + string scopes, + string? logo, + ICollection redirectUrl, + CancellationToken token = default); + + /// + /// Register a UdapClient in the Authorization Server. + /// To pick a different community the client can add a community query parameter. + /// + /// + /// + /// + /// + /// + /// + Task RegisterClientAuthCode( + X509Certificate2 certificate, + string scopes, + string? logo, + ICollection redirectUrl, + CancellationToken token = default); + Task ExchangeCodeForAuthTokenResponse(UdapAuthorizationCodeTokenRequest tokenRequest, CancellationToken token = default); Task?> ResolveJwtKeys(DiscoveryDocumentRequest? request = null, CancellationToken cancellationToken = default); diff --git a/Udap.Client/Client/UdapClient.cs b/Udap.Client/Client/UdapClient.cs index 88d39562..7265fcc3 100644 --- a/Udap.Client/Client/UdapClient.cs +++ b/Udap.Client/Client/UdapClient.cs @@ -92,93 +92,93 @@ 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()) - { - throw new Exception("Tiered OAuth: No client certificates provided."); - } + var resultDocument = await RegisterAuthCodeFlow(certificates, scopes, _udapClientOptions.TieredOAuthClientLogo, new List{ redirectUrl }, token); - foreach (var clientCert in x509Certificates) + if(string.IsNullOrEmpty(resultDocument.GetError())) { - _logger.LogDebug($"Using certificate {clientCert.SubjectName.Name} [ {clientCert.Thumbprint} ]"); - - var logoUrl = _udapClientOptions.TieredOAuthClientLogo; - - var document = 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(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("Tiered OAuth Client: Unable to register client to {RegistrationEndpoint}", this.UdapServerMetaData?.RegistrationEndpoint); + } - var response = await _httpClient.PostAsync(this.UdapServerMetaData?.RegistrationEndpoint, content, token); + return resultDocument; + } + catch(Exception ex) + { + _logger.LogError(ex, "Tiered OAuth Client: Unable to register client to {RegistrationEndpoint}", + this.UdapServerMetaData?.RegistrationEndpoint); + throw; + } + } - if (((int)response.StatusCode) < 500) - { - var resultDocument = - await response.Content.ReadFromJsonAsync(cancellationToken: token); + /// + /// Register a UdapClient in the Authorization Server. + /// To pick a different community the client can add a community query parameter. + /// + /// + /// + /// + /// + /// + /// + public async Task RegisterClientAuthCode( + IEnumerable certificates, + string scopes, + string? logo, + ICollection redirectUrl, + CancellationToken token = default) + { + if (this.UdapServerMetaData == null) + { + throw new Exception("UdapClient: UdapServerMetaData is null. Call ValidateResource first."); + } - if (resultDocument == null) - { - resultDocument = new UdapDynamicClientRegistrationDocument(); - resultDocument.Add("error", "Unknown error"); - } + try + { + var resultDocument = await RegisterAuthCodeFlow(certificates, scopes, logo, redirectUrl, token); - return resultDocument; - } + if (string.IsNullOrEmpty(resultDocument.GetError())) + { + _logger.LogWarning("UdapClient: Unable to register 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 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}"); + /// + /// Register a UdapClient in the Authorization Server. + /// To pick a different community the client can add a community query parameter. + /// + /// + /// + /// + /// + /// + /// + public async Task RegisterClientAuthCode( + X509Certificate2 certificate, + string scopes, + string? logo, + ICollection redirectUrl, + CancellationToken token = default) + { + return await this.RegisterClientAuthCode( + new List { certificate }, + scopes, + logo, + redirectUrl, + token + ); } /// @@ -257,7 +257,7 @@ private async Task InternalValidateResource( string? community, DiscoveryPolicy? discoveryPolicy, CancellationToken token = default) - { + { baseUrl.AssertUri(); @@ -369,6 +369,94 @@ private void NotifyTokenError(string message) } } } + + private async Task RegisterAuthCodeFlow( + IEnumerable certificates, + string scopes, + string? logoUrl, + ICollection? redirectUrls, + CancellationToken token) + { + var x509Certificates = certificates.ToList(); + if (certificates == null || !x509Certificates.Any()) + { + throw new Exception("Tiered OAuth: No client certificates provided."); + } + + UdapDynamicClientRegistrationDocument? resultDocument; + + foreach (var clientCert in x509Certificates) + { + _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) + .WithLogoUri(logoUrl) + .WithContacts(_udapClientOptions.Contacts) + .WithTokenEndpointAuthMethod(UdapConstants.RegistrationDocumentValues.TokenEndpointAuthMethodValue) + .WithScope(scopes) + .WithResponseTypes(new List { "code" }) + .WithRedirectUrls(redirectUrls) + .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(); + resultDocument.Add("error", "Unknown error"); + resultDocument.Add("error_description", response.StatusCode); + } + + return resultDocument; + } + } + + resultDocument = new UdapDynamicClientRegistrationDocument(); + resultDocument.Add("error", "Unknown error"); + resultDocument.Add("error_description", "Failed to register with all client certificates"); + + return resultDocument; + } + } - + } diff --git a/Udap.Model/Registration/UdapDcrBuilderForAuthorizationCode.cs b/Udap.Model/Registration/UdapDcrBuilderForAuthorizationCode.cs index d777418e..b9260ab2 100644 --- a/Udap.Model/Registration/UdapDcrBuilderForAuthorizationCode.cs +++ b/Udap.Model/Registration/UdapDcrBuilderForAuthorizationCode.cs @@ -9,7 +9,6 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Security.Cryptography.X509Certificates; using IdentityModel; using Microsoft.IdentityModel.Tokens; @@ -72,22 +71,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 +91,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. /// @@ -218,15 +193,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..dd587090 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; @@ -225,12 +198,16 @@ public UdapDcrBuilderForClientCredentials WithScope(string? scope) 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 58802403..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; + // } + // } /// @@ -539,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. @@ -705,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/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/_tests/UdapServer.Tests/Common/TestExtensions.cs b/_tests/UdapServer.Tests/Common/TestExtensions.cs index e5833980..7ea592db 100644 --- a/_tests/UdapServer.Tests/Common/TestExtensions.cs +++ b/_tests/UdapServer.Tests/Common/TestExtensions.cs @@ -8,7 +8,6 @@ #endregion using System.Diagnostics; -using Duende.IdentityServer.Configuration; using Microsoft.AspNetCore.Authentication; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -16,8 +15,6 @@ using Microsoft.Extensions.Options; using Udap.Client.Client; using Udap.Client.Configuration; -using Udap.Server.Hosting.DynamicProviders.Oidc; -using Udap.Server.Models; using Udap.Server.Security.Authentication.TieredOAuth; namespace UdapServer.Tests.Common; @@ -35,6 +32,7 @@ public static class TestExtensions public static AuthenticationBuilder AddTieredOAuthForTests( this AuthenticationBuilder builder, Action configuration, + UdapAuthServerPipeline authServerPipeline, UdapIdentityServerPipeline pipelineIdp1, UdapIdentityServerPipeline pipelineIdp2) { @@ -62,8 +60,12 @@ public static AuthenticationBuilder AddTieredOAuthForTests( sp.GetRequiredService>()); } - throw new ArgumentException( - "Must register a DynamicIdp in test with a Name property matching one of the UdapIdentityServerPipeline instances"); + return new UdapClient( + authServerPipeline.BrowserClient, + sp.GetRequiredService(), + sp.GetRequiredService>(), + sp.GetRequiredService>()); + }); builder.Services.TryAddSingleton(); diff --git a/_tests/UdapServer.Tests/Conformance/Basic/ScopeExpansionTests.cs b/_tests/UdapServer.Tests/Conformance/Basic/ScopeExpansionTests.cs index 726277ea..dc874e6b 100644 --- a/_tests/UdapServer.Tests/Conformance/Basic/ScopeExpansionTests.cs +++ b/_tests/UdapServer.Tests/Conformance/Basic/ScopeExpansionTests.cs @@ -14,29 +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; -using System.IdentityModel.Tokens.Jwt; namespace UdapServer.Tests.Conformance.Basic; @@ -45,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) { @@ -56,14 +54,14 @@ 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" } @@ -101,7 +99,7 @@ public ScopeExpansionTests(ITestOutputHelper testOutputHelper) Enabled = true, Intermediates = new List() { - new Intermediate + new() { BeginDate = intermediateCert.NotBefore.ToUniversalTime(), EndDate = intermediateCert.NotAfter.ToUniversalTime(), @@ -128,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"), @@ -164,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(), @@ -205,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(), @@ -247,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(), @@ -284,59 +282,18 @@ 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(), @@ -381,13 +338,13 @@ public async Task ScopeV2WithClientCredentialsExtendedTest() 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(), @@ -429,13 +386,13 @@ public async Task ScopeV2WithClientCredentialsExtended2Test() 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(), @@ -477,13 +434,13 @@ public async Task ScopeV2WithClientCredentialsWildcardTest() 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(), @@ -565,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", @@ -578,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/Tiered/TieredOauthTests.cs b/_tests/UdapServer.Tests/Conformance/Tiered/TieredOauthTests.cs index 3c1215fd..cf8845fc 100644 --- a/_tests/UdapServer.Tests/Conformance/Tiered/TieredOauthTests.cs +++ b/_tests/UdapServer.Tests/Conformance/Tiered/TieredOauthTests.cs @@ -20,6 +20,7 @@ using Duende.IdentityServer.Models; using Duende.IdentityServer.Test; using FluentAssertions; +using FluentAssertions.Common; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.OpenIdConnect; using Microsoft.AspNetCore.Builder; @@ -28,6 +29,7 @@ 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; @@ -76,9 +78,9 @@ public TieredOauthTests(ITestOutputHelper testOutputHelper) private void BuildUdapAuthorizationServer() { - _mockAuthorServerPipeline.OnPostConfigureServices += s => + _mockAuthorServerPipeline.OnPostConfigureServices += services => { - s.AddSingleton(new ServerSettings + services.AddSingleton(new ServerSettings { ServerSupport = ServerSupport.Hl7SecurityIG, // DefaultUserScopes = "udap", @@ -86,7 +88,7 @@ private void BuildUdapAuthorizationServer() // ForceStateParamOnAuthorizationCode = false (default) }); - s.AddSingleton>(new OptionsMonitorForTests( + services.AddSingleton>(new OptionsMonitorForTests( new UdapClientOptions { ClientName = "AuthServer Client", @@ -94,6 +96,11 @@ private void BuildUdapAuthorizationServer() TieredOAuthClientLogo = "https://server/udap.logo.48x48.png" }) ); + + // + // Allow logo resolve back to udap.auth server + // + services.AddSingleton(sp => _mockAuthorServerPipeline.BrowserClient); }; _mockAuthorServerPipeline.OnPreConfigureServices += (builderContext, services) => @@ -114,6 +121,7 @@ private void BuildUdapAuthorizationServer() { options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme; }, + _mockAuthorServerPipeline, _mockIdPPipeline, _mockIdPPipeline2); @@ -409,9 +417,6 @@ private void BuildUdapIdentityProvider2() [Fact] public async Task ClientAuthorize_IdPDiscovery_IdPRegistration_IdPAuthAccess_ClientAuthAccess_Test() { - var dynamicIdp = _mockAuthorServerPipeline.ApplicationServices.GetRequiredService(); - dynamicIdp.Name = _mockIdPPipeline.BaseUrl; - // Register client with auth server var resultDocument = await RegisterClientWithAuthServer(); _mockAuthorServerPipeline.RemoveSessionCookie(); @@ -421,6 +426,8 @@ public async Task ClientAuthorize_IdPDiscovery_IdPRegistration_IdPAuthAccess_Cli var clientId = resultDocument.ClientId!; + var dynamicIdp = _mockAuthorServerPipeline.ApplicationServices.GetRequiredService(); + dynamicIdp.Name = _mockIdPPipeline.BaseUrl; ////////////////////// // ClientAuthorize @@ -702,9 +709,6 @@ public async Task ClientAuthorize_IdPDiscovery_IdPRegistration_IdPAuthAccess_Cli [Fact] //(Skip = "Dynamic Tiered OAuth Provider WIP")] public async Task Tiered_OAuth_With_DynamicProvider() { - var dynamicIdp = _mockAuthorServerPipeline.ApplicationServices.GetRequiredService(); - dynamicIdp.Name = _mockIdPPipeline2.BaseUrl; - // Register client with auth server var resultDocument = await RegisterClientWithAuthServer(); _mockAuthorServerPipeline.RemoveSessionCookie(); @@ -714,7 +718,9 @@ public async Task Tiered_OAuth_With_DynamicProvider() var clientId = resultDocument.ClientId!; - + var dynamicIdp = _mockAuthorServerPipeline.ApplicationServices.GetRequiredService(); + dynamicIdp.Name = _mockIdPPipeline2.BaseUrl; + ////////////////////// // ClientAuthorize ////////////////////// @@ -991,44 +997,24 @@ public async Task Tiered_OAuth_With_DynamicProvider() { var clientCert = new X509Certificate2("CertStore/issued/fhirLabsApiClientLocalhostCert.pfx", "udap-test"); - // await _mockAuthorServerPipeline.LoginAsync("bob"); + var udapClient = _mockAuthorServerPipeline.Resolve(); - var document = UdapDcrBuilderForAuthorizationCode - .Create(clientCert) - .WithAudience(UdapAuthServerPipeline.RegistrationEndpoint) - .WithExpiration(TimeSpan.FromMinutes(5)) - .WithJwtId() - .WithClientName("mock tiered test") - .WithLogoUri("https://avatars.githubusercontent.com/u/77421324?s=48&v=4") - .WithContacts(new HashSet - { - "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(); - - 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 response = await _mockAuthorServerPipeline.BrowserClient.PostAsync( - UdapAuthServerPipeline.RegistrationEndpoint, - new StringContent(JsonSerializer.Serialize(requestBody), new MediaTypeHeaderValue("application/json"))); - response.StatusCode.Should().Be(HttpStatusCode.Created); - var resultDocument = await response.Content.ReadFromJsonAsync(); + var documentResponse = await udapClient.RegisterClientAuthCode( + clientCert, + "udap openid user/*.read", + "https://server/udap.logo.48x48.png", + new List { "https://code_client/callback" }, + CancellationToken.None); - return resultDocument; + documentResponse.GetError().Should().BeNull(); + + return documentResponse; } } \ No newline at end of file diff --git a/_tests/UdapServer.Tests/Validators/UdapDCRValidatorTests.cs b/_tests/UdapServer.Tests/Validators/UdapDCRValidatorTests.cs index efd4e612..3c20d1f0 100644 --- a/_tests/UdapServer.Tests/Validators/UdapDCRValidatorTests.cs +++ b/_tests/UdapServer.Tests/Validators/UdapDCRValidatorTests.cs @@ -13,9 +13,7 @@ using IdentityModel; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Primitives; using Microsoft.IdentityModel.Tokens; -using Microsoft.Net.Http.Headers; using Moq; using Moq.Protected; using Udap.Common.Certificates; diff --git a/examples/clients/UdapEd/Server/Controllers/RegisterController.cs b/examples/clients/UdapEd/Server/Controllers/RegisterController.cs index 31da90f5..e250f08d 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; @@ -201,13 +200,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 +274,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 +340,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 +405,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!); @@ -441,9 +438,11 @@ public async Task Register([FromBody] RegistrationRequest request DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }), 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 +472,5 @@ await response.Content.ReadAsStringAsync(), return BadRequest(ex); } - - return NotFound(); } } \ No newline at end of file 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); From 9e9b577515053bd1e6612911a571149aa31794ea Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 24 Oct 2023 12:20:52 +0000 Subject: [PATCH 28/42] Bump Microsoft.Extensions.Http.Polly from 7.0.12 to 7.0.13 Bumps [Microsoft.Extensions.Http.Polly](https://github.com/dotnet/aspnetcore) from 7.0.12 to 7.0.13. - [Release notes](https://github.com/dotnet/aspnetcore/releases) - [Changelog](https://github.com/dotnet/aspnetcore/blob/main/docs/ReleasePlanning.md) - [Commits](https://github.com/dotnet/aspnetcore/commits) --- updated-dependencies: - dependency-name: Microsoft.Extensions.Http.Polly dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- examples/Udap.Auth.Server.Admin/Udap.Auth.Server.Admin.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 0a0a0158..b53a4ed2 100644 --- a/examples/Udap.Auth.Server.Admin/Udap.Auth.Server.Admin.csproj +++ b/examples/Udap.Auth.Server.Admin/Udap.Auth.Server.Admin.csproj @@ -18,7 +18,7 @@ - + From 24273c7b193538dc36b3b66fc8f2a18e00cf6e07 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 24 Oct 2023 12:21:27 +0000 Subject: [PATCH 29/42] Bump Microsoft.AspNetCore.Authentication.JwtBearer from 7.0.12 to 7.0.13 Bumps [Microsoft.AspNetCore.Authentication.JwtBearer](https://github.com/dotnet/aspnetcore) from 7.0.12 to 7.0.13. - [Release notes](https://github.com/dotnet/aspnetcore/releases) - [Changelog](https://github.com/dotnet/aspnetcore/blob/main/docs/ReleasePlanning.md) - [Commits](https://github.com/dotnet/aspnetcore/commits) --- updated-dependencies: - dependency-name: Microsoft.AspNetCore.Authentication.JwtBearer dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- examples/FhirLabsApi/FhirLabsApi.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/FhirLabsApi/FhirLabsApi.csproj b/examples/FhirLabsApi/FhirLabsApi.csproj index 2f2ae9a4..0916f393 100644 --- a/examples/FhirLabsApi/FhirLabsApi.csproj +++ b/examples/FhirLabsApi/FhirLabsApi.csproj @@ -1,4 +1,4 @@ - + net7.0 @@ -47,7 +47,7 @@ - + From 397cc46f05c2927f5ee164fb20aa2ff022d64f92 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 24 Oct 2023 12:21:40 +0000 Subject: [PATCH 30/42] Bump Microsoft.AspNetCore.Razor.Language from 6.0.23 to 6.0.24 Bumps [Microsoft.AspNetCore.Razor.Language](https://github.com/dotnet/aspnetcore) from 6.0.23 to 6.0.24. - [Release notes](https://github.com/dotnet/aspnetcore/releases) - [Changelog](https://github.com/dotnet/aspnetcore/blob/main/docs/ReleasePlanning.md) - [Commits](https://github.com/dotnet/aspnetcore/commits) --- updated-dependencies: - dependency-name: Microsoft.AspNetCore.Razor.Language dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- examples/clients/UdapEd/Shared/UdapEd.Shared.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/clients/UdapEd/Shared/UdapEd.Shared.csproj b/examples/clients/UdapEd/Shared/UdapEd.Shared.csproj index 1b81887d..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 @@ - + From 05ea404ccbf50ca1ed6db605b4a9a83da519e771 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 24 Oct 2023 12:22:01 +0000 Subject: [PATCH 31/42] Bump Microsoft.AspNetCore.Components.WebAssembly.DevServer Bumps [Microsoft.AspNetCore.Components.WebAssembly.DevServer](https://github.com/dotnet/aspnetcore) from 7.0.12 to 7.0.13. - [Release notes](https://github.com/dotnet/aspnetcore/releases) - [Changelog](https://github.com/dotnet/aspnetcore/blob/main/docs/ReleasePlanning.md) - [Commits](https://github.com/dotnet/aspnetcore/commits) --- updated-dependencies: - dependency-name: Microsoft.AspNetCore.Components.WebAssembly.DevServer dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- examples/clients/UdapEd/Client/UdapEd.Client.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/clients/UdapEd/Client/UdapEd.Client.csproj b/examples/clients/UdapEd/Client/UdapEd.Client.csproj index 5082d14f..f0720b15 100644 --- a/examples/clients/UdapEd/Client/UdapEd.Client.csproj +++ b/examples/clients/UdapEd/Client/UdapEd.Client.csproj @@ -1,4 +1,4 @@ - + net7.0 @@ -19,7 +19,7 @@ - + From d77504ff22f27c19a5604d0115eddaa465af02a7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 24 Oct 2023 12:22:21 +0000 Subject: [PATCH 32/42] Bump dotnet-ef from 7.0.12 to 7.0.13 Bumps [dotnet-ef](https://github.com/dotnet/efcore) from 7.0.12 to 7.0.13. - [Release notes](https://github.com/dotnet/efcore/releases) - [Commits](https://github.com/dotnet/efcore/commits) --- updated-dependencies: - dependency-name: dotnet-ef dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .config/dotnet-tools.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 2f789b03..dbe78984 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "dotnet-ef": { - "version": "7.0.12", + "version": "7.0.13", "commands": [ "dotnet-ef" ] From ddbc49d5baa93fbe61dff1a802b1d35ecf1e7887 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 24 Oct 2023 12:22:38 +0000 Subject: [PATCH 33/42] Bump Microsoft.AspNetCore.Components.WebAssembly.Server Bumps [Microsoft.AspNetCore.Components.WebAssembly.Server](https://github.com/dotnet/aspnetcore) from 7.0.12 to 7.0.13. - [Release notes](https://github.com/dotnet/aspnetcore/releases) - [Changelog](https://github.com/dotnet/aspnetcore/blob/main/docs/ReleasePlanning.md) - [Commits](https://github.com/dotnet/aspnetcore/commits) --- updated-dependencies: - dependency-name: Microsoft.AspNetCore.Components.WebAssembly.Server dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- examples/clients/UdapEd/Server/UdapEd.Server.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/clients/UdapEd/Server/UdapEd.Server.csproj b/examples/clients/UdapEd/Server/UdapEd.Server.csproj index cad9a484..64dc9384 100644 --- a/examples/clients/UdapEd/Server/UdapEd.Server.csproj +++ b/examples/clients/UdapEd/Server/UdapEd.Server.csproj @@ -21,7 +21,7 @@ - + From c41dddbde953b0e39bc9e5ee29448504a26c2f03 Mon Sep 17 00:00:00 2001 From: Joseph Shook Date: Tue, 24 Oct 2023 08:26:23 -0700 Subject: [PATCH 34/42] Continued building and using UdapClient Registration interfaces. Included RegisterClientCredentialsClient Promoted ExchangeCodeForTokenResponse to IUdapClient interface. --- Udap.Client/Client/IUdapClient.cs | 54 +++- Udap.Client/Client/UdapClient.cs | 237 +++++++++++--- ...TokenRequestForClientCredentialsBuilder.cs | 1 - .../UdapDcrBuilderForAuthorizationCode.cs | 25 +- .../UdapDcrBuilderForClientCredentials.cs | 2 +- .../Common/UdapAuthServerPipeline.cs | 2 +- .../Basic/ClientCredentialsUdapModeTests.cs | 292 +++++------------- .../Conformance/Tiered/TieredOauthTests.cs | 27 +- examples/Udap.Auth.Server/Pages/TestUsers.cs | 2 +- 9 files changed, 352 insertions(+), 290 deletions(-) diff --git a/Udap.Client/Client/IUdapClient.cs b/Udap.Client/Client/IUdapClient.cs index 58c9aa42..3fb1dde0 100644 --- a/Udap.Client/Client/IUdapClient.cs +++ b/Udap.Client/Client/IUdapClient.cs @@ -55,39 +55,75 @@ Task RegisterTieredClient(string redirect CancellationToken token = default); /// - /// Register a UdapClient in the Authorization Server. - /// To pick a different community the client can add a community query parameter. + /// 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 RegisterClientAuthCode( + Task RegisterAuthCodeClient( IEnumerable certificates, string scopes, - string? logo, + string logo, ICollection redirectUrl, + string? issuer = null, CancellationToken token = default); /// - /// Register a UdapClient in the Authorization Server. - /// To pick a different community the client can add a community query parameter. + /// 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 RegisterClientAuthCode( + Task RegisterAuthCodeClient( X509Certificate2 certificate, string scopes, - string? logo, + 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); diff --git a/Udap.Client/Client/UdapClient.cs b/Udap.Client/Client/UdapClient.cs index 7265fcc3..fbe01eb4 100644 --- a/Udap.Client/Client/UdapClient.cs +++ b/Udap.Client/Client/UdapClient.cs @@ -97,7 +97,7 @@ public async Task RegisterTieredClient(st try { - var resultDocument = await RegisterAuthCodeFlow(certificates, scopes, _udapClientOptions.TieredOAuthClientLogo, new List{ redirectUrl }, token); + var resultDocument = await RegisterAuthCodeFlow(certificates, scopes, _udapClientOptions.TieredOAuthClientLogo, new List{ redirectUrl }, null, token); if(string.IsNullOrEmpty(resultDocument.GetError())) { @@ -114,21 +114,13 @@ public async Task RegisterTieredClient(st } } - /// - /// Register a UdapClient in the Authorization Server. - /// To pick a different community the client can add a community query parameter. - /// - /// - /// - /// - /// - /// - /// - public async Task RegisterClientAuthCode( + /// + public async Task RegisterAuthCodeClient( IEnumerable certificates, string scopes, - string? logo, + string logo, ICollection redirectUrl, + string? issuer, CancellationToken token = default) { if (this.UdapServerMetaData == null) @@ -138,45 +130,87 @@ public async Task RegisterClientAuthCode( try { - var resultDocument = await RegisterAuthCodeFlow(certificates, scopes, logo, redirectUrl, token); + var resultDocument = await RegisterAuthCodeFlow(certificates, scopes, logo, redirectUrl, issuer, token); if (string.IsNullOrEmpty(resultDocument.GetError())) { - _logger.LogWarning("UdapClient: Unable to register client to {RegistrationEndpoint}", this.UdapServerMetaData?.RegistrationEndpoint); + _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 client to {RegistrationEndpoint}", + _logger.LogError(ex, "UdapClient: Unable to register authorization_code client to {RegistrationEndpoint}", this.UdapServerMetaData?.RegistrationEndpoint); throw; } } - /// - /// Register a UdapClient in the Authorization Server. - /// To pick a different community the client can add a community query parameter. - /// - /// - /// - /// - /// - /// - /// - public async Task RegisterClientAuthCode( + /// + public async Task RegisterAuthCodeClient( X509Certificate2 certificate, string scopes, - string? logo, + string logo, ICollection redirectUrl, + string? issuer, CancellationToken token = default) { - return await this.RegisterClientAuthCode( + return await this.RegisterAuthCodeClient( new List { certificate }, scopes, logo, redirectUrl, + issuer, + 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."); + } + + 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) + { + _logger.LogError(ex, "UdapClient: Unable to register client_credentials client to {RegistrationEndpoint}", + this.UdapServerMetaData?.RegistrationEndpoint); + throw; + } + } + + //// + 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 ); } @@ -186,7 +220,7 @@ public async Task RegisterClientAuthCode( /// /// The request. /// The cancellation token. - /// + /// public async Task ExchangeCodeForTokenResponse( UdapAuthorizationCodeTokenRequest tokenRequest, CancellationToken token = default) @@ -230,7 +264,7 @@ public Task ValidateResource( DiscoveryPolicy? discoveryPolicy = null, CancellationToken token = default) { - return InternalValidateResource(baseUrl, trustAnchorStore, community, discoveryPolicy); + return InternalValidateResource(baseUrl, trustAnchorStore, community, discoveryPolicy, token); } /// @@ -248,7 +282,7 @@ public async Task ValidateResource( DiscoveryPolicy? discoveryPolicy, CancellationToken token = default) { - return await InternalValidateResource(baseUrl, null, community, discoveryPolicy); + return await InternalValidateResource(baseUrl, null, community, discoveryPolicy, token); } private async Task InternalValidateResource( @@ -304,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) @@ -373,8 +407,9 @@ private void NotifyTokenError(string message) private async Task RegisterAuthCodeFlow( IEnumerable certificates, string scopes, - string? logoUrl, + string logoUrl, ICollection? redirectUrls, + string? issuer, CancellationToken token) { var x509Certificates = certificates.ToList(); @@ -383,13 +418,18 @@ private async Task RegisterAuthCodeFlow( 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 document = UdapDcrBuilderForAuthorizationCode + + var builder = UdapDcrBuilderForAuthorizationCode .Create(clientCert) .WithAudience(this.UdapServerMetaData?.RegistrationEndpoint) .WithExpiration(TimeSpan.FromMinutes(5)) @@ -400,9 +440,114 @@ private async Task RegisterAuthCodeFlow( .WithTokenEndpointAuthMethod(UdapConstants.RegistrationDocumentValues.TokenEndpointAuthMethodValue) .WithScope(scopes) .WithResponseTypes(new List { "code" }) - .WithRedirectUrls(redirectUrls) - .Build(); + .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 @@ -441,18 +586,22 @@ await response.Content.ReadFromJsonAsync( if (resultDocument == null) { - resultDocument = new UdapDynamicClientRegistrationDocument(); - resultDocument.Add("error", "Unknown error"); - resultDocument.Add("error_description", response.StatusCode); + resultDocument = new UdapDynamicClientRegistrationDocument + { + { "error", "Unknown error" }, + { "error_description", response.StatusCode } + }; } return resultDocument; } } - resultDocument = new UdapDynamicClientRegistrationDocument(); - resultDocument.Add("error", "Unknown error"); - resultDocument.Add("error_description", "Failed to register with all client certificates"); + resultDocument = new UdapDynamicClientRegistrationDocument + { + { "error", "Unknown error" }, + { "error_description", "Failed to register with all client certificates" } + }; return resultDocument; } 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 b9260ab2..268fbdac 100644 --- a/Udap.Model/Registration/UdapDcrBuilderForAuthorizationCode.cs +++ b/Udap.Model/Registration/UdapDcrBuilderForAuthorizationCode.cs @@ -9,10 +9,12 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Security.Cryptography.X509Certificates; using IdentityModel; using Microsoft.IdentityModel.Tokens; using Udap.Model.Statement; +using Udap.Util.Extensions; namespace Udap.Model.Registration; @@ -117,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; @@ -157,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; diff --git a/Udap.Model/Registration/UdapDcrBuilderForClientCredentials.cs b/Udap.Model/Registration/UdapDcrBuilderForClientCredentials.cs index dd587090..4f0f2d0c 100644 --- a/Udap.Model/Registration/UdapDcrBuilderForClientCredentials.cs +++ b/Udap.Model/Registration/UdapDcrBuilderForClientCredentials.cs @@ -196,7 +196,7 @@ public UdapDcrBuilderForClientCredentials WithScope(string? scope) return this; } - public UdapDcrBuilderForClientCredentials WithLogoUri(string logoUri) + public UdapDcrBuilderForClientCredentials WithLogoUri(string? logoUri) { if (string.IsNullOrEmpty(logoUri)) { diff --git a/_tests/UdapServer.Tests/Common/UdapAuthServerPipeline.cs b/_tests/UdapServer.Tests/Common/UdapAuthServerPipeline.cs index b421bf79..958f9b7a 100644 --- a/_tests/UdapServer.Tests/Common/UdapAuthServerPipeline.cs +++ b/_tests/UdapServer.Tests/Common/UdapAuthServerPipeline.cs @@ -656,5 +656,5 @@ public T Resolve() public class DynamicIdp { - public string Name { get; set; } + public string? Name { get; set; } } \ No newline at end of file diff --git a/_tests/UdapServer.Tests/Conformance/Basic/ClientCredentialsUdapModeTests.cs b/_tests/UdapServer.Tests/Conformance/Basic/ClientCredentialsUdapModeTests.cs index 6c0ed2bf..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>())); }; @@ -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,81 +329,24 @@ 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(); - - 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.GetError().Should().BeNull(); regDocumentResult!.Scope.Should().Be("system/Appointment.rs system/Patient.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(); + 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() @@ -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 diff --git a/_tests/UdapServer.Tests/Conformance/Tiered/TieredOauthTests.cs b/_tests/UdapServer.Tests/Conformance/Tiered/TieredOauthTests.cs index cf8845fc..7619b18e 100644 --- a/_tests/UdapServer.Tests/Conformance/Tiered/TieredOauthTests.cs +++ b/_tests/UdapServer.Tests/Conformance/Tiered/TieredOauthTests.cs @@ -10,8 +10,6 @@ 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,13 +18,11 @@ using Duende.IdentityServer.Models; using Duende.IdentityServer.Test; using FluentAssertions; -using FluentAssertions.Common; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.OpenIdConnect; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; using Moq; @@ -38,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; @@ -100,7 +95,7 @@ private void BuildUdapAuthorizationServer() // // Allow logo resolve back to udap.auth server // - services.AddSingleton(sp => _mockAuthorServerPipeline.BrowserClient); + services.AddSingleton(_ => _mockAuthorServerPipeline.BrowserClient); }; _mockAuthorServerPipeline.OnPreConfigureServices += (builderContext, services) => @@ -644,11 +639,9 @@ 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(); @@ -936,11 +929,8 @@ public async Task Tiered_OAuth_With_DynamicProvider() .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(); @@ -1006,12 +996,11 @@ public async Task Tiered_OAuth_With_DynamicProvider() { RegistrationEndpoint = UdapAuthServerPipeline.RegistrationEndpoint }; - var documentResponse = await udapClient.RegisterClientAuthCode( + var documentResponse = await udapClient.RegisterAuthCodeClient( clientCert, "udap openid user/*.read", "https://server/udap.logo.48x48.png", - new List { "https://code_client/callback" }, - CancellationToken.None); + new List { "https://code_client/callback" }); documentResponse.GetError().Should().BeNull(); 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 = { From 72184879dc7b315670ccedc04d4f6006e5cfa77d Mon Sep 17 00:00:00 2001 From: Joseph Shook Date: Tue, 24 Oct 2023 08:26:56 -0700 Subject: [PATCH 35/42] Add client UDAP certificate check and alert for expired certificate. --- .../UdapEd/Client/Shared/ClientCertLoader.razor | 11 +++++++++++ .../UdapEd/Server/Controllers/RegisterController.cs | 11 +++++++++++ .../clients/UdapEd/Shared/Model/CertLoadedEnum.cs | 2 +- 3 files changed, 23 insertions(+), 1 deletion(-) 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/Server/Controllers/RegisterController.cs b/examples/clients/UdapEd/Server/Controllers/RegisterController.cs index e250f08d..d7c01817 100644 --- a/examples/clients/UdapEd/Server/Controllers/RegisterController.cs +++ b/examples/clients/UdapEd/Server/Controllers/RegisterController.cs @@ -106,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) @@ -152,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) 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 From fc82782e05542f36149cc9549c3515fed8f93799 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 24 Oct 2023 15:35:27 +0000 Subject: [PATCH 36/42] Bump Microsoft.AspNetCore.Components.WebAssembly from 7.0.12 to 7.0.13 Bumps [Microsoft.AspNetCore.Components.WebAssembly](https://github.com/dotnet/aspnetcore) from 7.0.12 to 7.0.13. - [Release notes](https://github.com/dotnet/aspnetcore/releases) - [Changelog](https://github.com/dotnet/aspnetcore/blob/main/docs/ReleasePlanning.md) - [Commits](https://github.com/dotnet/aspnetcore/commits) --- updated-dependencies: - dependency-name: Microsoft.AspNetCore.Components.WebAssembly dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- examples/clients/UdapEd/Client/UdapEd.Client.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/clients/UdapEd/Client/UdapEd.Client.csproj b/examples/clients/UdapEd/Client/UdapEd.Client.csproj index f0720b15..bf48ccc1 100644 --- a/examples/clients/UdapEd/Client/UdapEd.Client.csproj +++ b/examples/clients/UdapEd/Client/UdapEd.Client.csproj @@ -18,7 +18,7 @@ - + From 38b145fbde26a0b9bdc732b77f24dd2bca3b131e Mon Sep 17 00:00:00 2001 From: Joseph Shook Date: Tue, 24 Oct 2023 08:50:57 -0700 Subject: [PATCH 37/42] Package updates --- Directory.Packages.props | 4 ++-- Udap.Server/Udap.Server.csproj | 10 +--------- _tests/Directory.Packages.props | 10 +++++----- .../Udap.Auth.Server.Admin.csproj | 6 +++--- examples/Udap.Auth.Server/Udap.Auth.Server.csproj | 6 +++--- examples/Udap.CA/Udap.CA.csproj | 8 ++++---- .../Udap.Identity.Provider.2.csproj | 6 +++--- .../Udap.Identity.Provider.csproj | 6 +++--- .../1_UdapClientMetadata/1_UdapClientMetadata.csproj | 2 +- .../2_UdapClientMetadata/2_UdapClientMetadata.csproj | 2 +- migrations/UdapDb.SqlServer/UdapDb.SqlServer.csproj | 6 +++--- 11 files changed, 29 insertions(+), 37 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index b7eaeb1a..54aacd5f 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -10,7 +10,7 @@ - + @@ -22,7 +22,7 @@ - + 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/_tests/Directory.Packages.props b/_tests/Directory.Packages.props index 52bc0e67..8a026f32 100644 --- a/_tests/Directory.Packages.props +++ b/_tests/Directory.Packages.props @@ -12,12 +12,12 @@ - + - - - - + + + + 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 b53a4ed2..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,9 +14,9 @@ - - - + + + diff --git a/examples/Udap.Auth.Server/Udap.Auth.Server.csproj b/examples/Udap.Auth.Server/Udap.Auth.Server.csproj index 2ecf2f85..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.CA/Udap.CA.csproj b/examples/Udap.CA/Udap.CA.csproj index ffee8324..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/Udap.Identity.Provider.2.csproj b/examples/Udap.Identity.Provider.2/Udap.Identity.Provider.2.csproj index b49701cd..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/Udap.Identity.Provider.csproj b/examples/Udap.Identity.Provider/Udap.Identity.Provider.csproj index f2fb04ca..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 - - + + diff --git a/examples/clients/1_UdapClientMetadata/1_UdapClientMetadata.csproj b/examples/clients/1_UdapClientMetadata/1_UdapClientMetadata.csproj index b8aeacf6..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 14e00772..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/migrations/UdapDb.SqlServer/UdapDb.SqlServer.csproj b/migrations/UdapDb.SqlServer/UdapDb.SqlServer.csproj index 0af87cd3..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 - - + + From 1fca44ff9eda777776efe454960d267b1fe3f2c7 Mon Sep 17 00:00:00 2001 From: Joseph Shook Date: Tue, 24 Oct 2023 08:55:30 -0700 Subject: [PATCH 38/42] Package updates/cleanup --- Directory.Packages.props | 2 +- .../Properties/Directory.Packages.props | 33 ------------------- 2 files changed, 1 insertion(+), 34 deletions(-) delete mode 100644 migrations/UdapDb.SqlServer/Properties/Directory.Packages.props diff --git a/Directory.Packages.props b/Directory.Packages.props index 54aacd5f..226c204d 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -23,7 +23,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 From f4bcbec399a0ac49144df11eddf6f4d3f692c724 Mon Sep 17 00:00:00 2001 From: Joseph Shook Date: Tue, 24 Oct 2023 08:58:15 -0700 Subject: [PATCH 39/42] Cleanup --- examples/Directory.Packages.props | 30 ------------------------------ 1 file changed, 30 deletions(-) delete mode 100644 examples/Directory.Packages.props diff --git a/examples/Directory.Packages.props b/examples/Directory.Packages.props deleted file mode 100644 index 06e46371..00000000 --- a/examples/Directory.Packages.props +++ /dev/null @@ -1,30 +0,0 @@ - - - true - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file From d6ee084e5161575d2357e37e6e7ed16cc29933f8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 25 Oct 2023 12:29:08 +0000 Subject: [PATCH 40/42] Bump Microsoft.EntityFrameworkCore.SqlServer from 7.0.12 to 7.0.13 Bumps [Microsoft.EntityFrameworkCore.SqlServer](https://github.com/dotnet/efcore) from 7.0.12 to 7.0.13. - [Release notes](https://github.com/dotnet/efcore/releases) - [Commits](https://github.com/dotnet/efcore/compare/v7.0.12...v7.0.13) --- updated-dependencies: - dependency-name: Microsoft.EntityFrameworkCore.SqlServer dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Directory.Packages.props | 2 +- _tests/Directory.Packages.props | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 226c204d..56d37387 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -24,7 +24,7 @@ - + diff --git a/_tests/Directory.Packages.props b/_tests/Directory.Packages.props index 8a026f32..ed916d6c 100644 --- a/_tests/Directory.Packages.props +++ b/_tests/Directory.Packages.props @@ -18,7 +18,7 @@ - + From f05a24622e2c400182fa1712d205edfb498d660e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 25 Oct 2023 12:29:28 +0000 Subject: [PATCH 41/42] Bump Microsoft.EntityFrameworkCore.Sqlite from 7.0.12 to 7.0.13 Bumps [Microsoft.EntityFrameworkCore.Sqlite](https://github.com/dotnet/efcore) from 7.0.12 to 7.0.13. - [Release notes](https://github.com/dotnet/efcore/releases) - [Commits](https://github.com/dotnet/efcore/compare/v7.0.12...v7.0.13) --- updated-dependencies: - dependency-name: Microsoft.EntityFrameworkCore.Sqlite dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 226c204d..08aab3b0 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -26,7 +26,7 @@ - + From e62a5c8df38329a2efe5ec83b8a823c040a7f7da Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 27 Oct 2023 12:39:07 +0000 Subject: [PATCH 42/42] Bump Microsoft.Data.SqlClient from 5.1.1 to 5.1.2 Bumps [Microsoft.Data.SqlClient](https://github.com/dotnet/sqlclient) from 5.1.1 to 5.1.2. - [Release notes](https://github.com/dotnet/sqlclient/releases) - [Changelog](https://github.com/dotnet/SqlClient/blob/main/CHANGELOG.md) - [Commits](https://github.com/dotnet/sqlclient/compare/v5.1.1...v5.1.2) --- updated-dependencies: - dependency-name: Microsoft.Data.SqlClient dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- _tests/Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_tests/Directory.Packages.props b/_tests/Directory.Packages.props index 8a026f32..d02e6258 100644 --- a/_tests/Directory.Packages.props +++ b/_tests/Directory.Packages.props @@ -13,7 +13,7 @@ - +