diff --git a/identity-server/identity-server.slnf b/identity-server/identity-server.slnf index c33e53e24..51f061c3a 100644 --- a/identity-server/identity-server.slnf +++ b/identity-server/identity-server.slnf @@ -2,6 +2,8 @@ "solution": { "path": "..\\products.slnx", "projects": [ + "conformance-report\\src\\ConformanceReport\\ConformanceReport.csproj", + "conformance-report\\test\\ConformanceReport.Tests\\ConformanceReport.Tests.csproj", "identity-server\\aspire\\AppHosts\\All\\All.csproj", "identity-server\\aspire\\AppHosts\\Dev\\Dev.csproj", "identity-server\\aspire\\ServiceDefaults\\ServiceDefaults.csproj", @@ -40,13 +42,13 @@ "identity-server\\clients\\src\\MvcJarUriJwt\\MvcJarUriJwt.csproj", "identity-server\\clients\\src\\Web\\Web.csproj", "identity-server\\clients\\src\\WindowsConsoleSystemBrowser\\WindowsConsoleSystemBrowser.csproj", + "identity-server\\hosts\\AspNetIdentity10\\Host.AspNetIdentity10.csproj", + "identity-server\\hosts\\EntityFramework10\\Host.EntityFramework10.csproj", + "identity-server\\hosts\\Main10\\Host.Main10.csproj", "identity-server\\hosts\\Shared\\Host.Shared.csproj", "identity-server\\hosts\\UI\\AspNetIdentity\\UI.AspNetIdentity.csproj", "identity-server\\hosts\\UI\\EntityFramework\\UI.EntityFramework.csproj", "identity-server\\hosts\\UI\\Main\\UI.Main.csproj", - "identity-server\\hosts\\AspNetIdentity10\\Host.AspNetIdentity10.csproj", - "identity-server\\hosts\\EntityFramework10\\Host.EntityFramework10.csproj", - "identity-server\\hosts\\Main10\\Host.Main10.csproj", "identity-server\\migrations\\AspNetIdentityDb\\AspNetIdentityDb.csproj", "identity-server\\migrations\\IdentityServerDb\\IdentityServerDb.csproj", "identity-server\\src\\AspNetIdentity\\IdentityServer.AspNetIdentity.csproj", @@ -54,6 +56,7 @@ "identity-server\\src\\Configuration\\IdentityServer.Configuration.csproj", "identity-server\\src\\EntityFramework.Storage\\IdentityServer.EntityFramework.Storage.csproj", "identity-server\\src\\EntityFramework\\IdentityServer.EntityFramework.csproj", + "identity-server\\src\\IdentityServer.ConformanceReport\\IdentityServer.ConformanceReport.csproj", "identity-server\\src\\IdentityServer\\IdentityServer.csproj", "identity-server\\src\\Storage\\IdentityServer.Storage.csproj", "identity-server\\templates\\src\\IdentityServerAspNetIdentity\\IdentityServerAspNetIdentity.csproj", @@ -69,4 +72,4 @@ "shared\\Xunit.Playwright\\Xunit.Playwright.csproj" ] } -} +} \ No newline at end of file diff --git a/identity-server/src/IdentityServer/IdentityServerConstants.cs b/identity-server/src/IdentityServer/IdentityServerConstants.cs index 129cbfb77..7d464ff27 100644 --- a/identity-server/src/IdentityServer/IdentityServerConstants.cs +++ b/identity-server/src/IdentityServer/IdentityServerConstants.cs @@ -279,8 +279,8 @@ public static class IdentityServerConstants public const string SamlPathPrefix = "saml"; public const string SamlMetadata = SamlPathPrefix + "/metadata"; - public const string SamlSignin = SamlPathPrefix + "/sso"; - public const string SamlSigninCallback = SamlSignin + "/callback"; + public const string SamlSignin = SamlPathPrefix + "/signin"; + public const string SamlSigninCallback = SamlPathPrefix + "/signin_callback"; public const string SamlIdpInitiated = SamlPathPrefix + "/idp-initiated"; public const string SamlLogout = SamlPathPrefix + "/slo"; public const string SamlLogoutCallback = SamlLogout + "/callback"; diff --git a/identity-server/test/IdentityServer.IntegrationTests/Endpoints/Saml/SamlClaimsMappingTests.cs b/identity-server/test/IdentityServer.IntegrationTests/Endpoints/Saml/SamlClaimsMappingTests.cs index 9f8f4849f..f8c42cf36 100644 --- a/identity-server/test/IdentityServer.IntegrationTests/Endpoints/Saml/SamlClaimsMappingTests.cs +++ b/identity-server/test/IdentityServer.IntegrationTests/Endpoints/Saml/SamlClaimsMappingTests.cs @@ -8,16 +8,15 @@ using Duende.IdentityModel; using Duende.IdentityServer.Configuration; using Duende.IdentityServer.Saml; using Microsoft.Extensions.DependencyInjection; -using Xunit.Abstractions; using static Duende.IdentityServer.IntegrationTests.Endpoints.Saml.SamlTestHelpers; namespace Duende.IdentityServer.IntegrationTests.Endpoints.Saml; -public class SamlClaimsMappingTests(ITestOutputHelper output) +public class SamlClaimsMappingTests { private const string Category = "SAML Claims Mapping"; - private SamlFixture Fixture = new(output); + private SamlFixture Fixture = new(); private SamlDataBuilder Build => Fixture.Builder; [Fact] diff --git a/identity-server/test/IdentityServer.IntegrationTests/Endpoints/Saml/SamlEncryptionTests.cs b/identity-server/test/IdentityServer.IntegrationTests/Endpoints/Saml/SamlEncryptionTests.cs index 8f5826f9b..d9477e1e5 100644 --- a/identity-server/test/IdentityServer.IntegrationTests/Endpoints/Saml/SamlEncryptionTests.cs +++ b/identity-server/test/IdentityServer.IntegrationTests/Endpoints/Saml/SamlEncryptionTests.cs @@ -9,17 +9,16 @@ using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using Duende.IdentityModel; using Duende.IdentityServer.Models; -using Xunit.Abstractions; using static Duende.IdentityServer.IntegrationTests.Endpoints.Saml.SamlTestHelpers; using SamlStatusCode = Duende.IdentityServer.Saml.Models.SamlStatusCode; namespace Duende.IdentityServer.IntegrationTests.Endpoints.Saml; -public class SamlEncryptionTests(ITestOutputHelper output) +public class SamlEncryptionTests { private const string Category = "SAML Encryption"; - private SamlFixture Fixture = new(output); + private SamlFixture Fixture = new(); private SamlData Data => Fixture.Data; private SamlDataBuilder Build => Fixture.Builder; diff --git a/identity-server/test/IdentityServer.IntegrationTests/Endpoints/Saml/SamlFixture.cs b/identity-server/test/IdentityServer.IntegrationTests/Endpoints/Saml/SamlFixture.cs index 9cd1c0881..da5924e9e 100644 --- a/identity-server/test/IdentityServer.IntegrationTests/Endpoints/Saml/SamlFixture.cs +++ b/identity-server/test/IdentityServer.IntegrationTests/Endpoints/Saml/SamlFixture.cs @@ -13,11 +13,10 @@ using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; -using Xunit.Abstractions; namespace Duende.IdentityServer.IntegrationTests.Endpoints.Saml; -internal class SamlFixture(ITestOutputHelper output) : IAsyncLifetime +internal class SamlFixture : IAsyncLifetime { public SamlData Data = new SamlData(); public SamlDataBuilder Builder => new SamlDataBuilder(Data); @@ -78,7 +77,7 @@ internal class SamlFixture(ITestOutputHelper output) : IAsyncLifetime public T Get() where T : notnull => Host!.Resolve(); - public async Task InitializeAsync() + public async ValueTask InitializeAsync() { var selfSignedCertificate = X509CertificateLoader.LoadPkcs12(Convert.FromBase64String(StableSigningCert), null); @@ -178,7 +177,7 @@ internal class SamlFixture(ITestOutputHelper output) : IAsyncLifetime NonRedirectingClient = Host!.Server.CreateClient(); } - public async Task DisposeAsync() + public async ValueTask DisposeAsync() { if (Host != null) { diff --git a/identity-server/test/IdentityServer.IntegrationTests/Endpoints/Saml/SamlIdpInitiatedEndpointTests.cs b/identity-server/test/IdentityServer.IntegrationTests/Endpoints/Saml/SamlIdpInitiatedEndpointTests.cs index 35afaddde..152bbb403 100644 --- a/identity-server/test/IdentityServer.IntegrationTests/Endpoints/Saml/SamlIdpInitiatedEndpointTests.cs +++ b/identity-server/test/IdentityServer.IntegrationTests/Endpoints/Saml/SamlIdpInitiatedEndpointTests.cs @@ -8,16 +8,15 @@ using System.Web; using Duende.IdentityModel; using Duende.IdentityServer.Internal.Saml; using Microsoft.AspNetCore.Mvc; -using Xunit.Abstractions; using static Duende.IdentityServer.IntegrationTests.Endpoints.Saml.SamlTestHelpers; namespace Duende.IdentityServer.IntegrationTests.Endpoints.Saml; -public class SamlIdpInitiatedEndpointTests(ITestOutputHelper output) +public class SamlIdpInitiatedEndpointTests { private const string Category = "SAML IdP-Initiated Endpoint"; - private SamlFixture Fixture = new(output); + private SamlFixture Fixture = new(); private SamlData Data => Fixture.Data; @@ -293,7 +292,7 @@ public class SamlIdpInitiatedEndpointTests(ITestOutputHelper output) var samlResponse = await ExtractSamlSuccessFromPostAsync(callbackResult, CancellationToken.None); samlResponse.ShouldNotBeNull(); - samlResponse.Issuer.ShouldBe(Fixture.Host!.Uri()); + samlResponse.Issuer.ShouldBe(Fixture.Host!.Url()); samlResponse.Destination.ShouldBe(Data.AcsUrl.ToString()); samlResponse.StatusCode.ShouldBe("urn:oasis:names:tc:SAML:2.0:status:Success"); diff --git a/identity-server/test/IdentityServer.IntegrationTests/Endpoints/Saml/SamlMetadataEndpointTests.cs b/identity-server/test/IdentityServer.IntegrationTests/Endpoints/Saml/SamlMetadataEndpointTests.cs index a4d78e1b9..292750882 100644 --- a/identity-server/test/IdentityServer.IntegrationTests/Endpoints/Saml/SamlMetadataEndpointTests.cs +++ b/identity-server/test/IdentityServer.IntegrationTests/Endpoints/Saml/SamlMetadataEndpointTests.cs @@ -4,15 +4,14 @@ using System.Net; using System.Xml.Linq; using Duende.IdentityServer.Internal.Saml; -using Xunit.Abstractions; namespace Duende.IdentityServer.IntegrationTests.Endpoints.Saml; -public class SamlMetadataEndpointTests(ITestOutputHelper output) +public class SamlMetadataEndpointTests { private const string Category = "SAML Metadata Endpoint"; - private SamlFixture Fixture = new(output); + private SamlFixture Fixture = new(); [Fact] [Trait("Category", Category)] @@ -30,7 +29,7 @@ public class SamlMetadataEndpointTests(ITestOutputHelper output) var content = await result.Content.ReadAsStringAsync(CancellationToken.None); var settings = new VerifySettings(); - var hostUri = Fixture.Host!.Uri(); + var hostUri = Fixture.Host!.Url(); settings.AddScrubber(sb => { sb.Replace(hostUri, "https://localhost"); @@ -189,7 +188,7 @@ public class SamlMetadataEndpointTests(ITestOutputHelper output) public async Task metadata_urls_should_handle_edge_case_route_configurations() { // Verify that default configuration produces clean URLs - // Edge cases (empty routes, whitespace, etc.) are comprehensively tested + // Edge cases (empty routes, whitespace, etc.) are comprehensively tested // at the unit level in BuildServiceUrl unit tests await Fixture.InitializeAsync(); diff --git a/identity-server/test/IdentityServer.IntegrationTests/Endpoints/Saml/SamlMetadataEndpointTests.metadata_endpoint_should_return_metadata.verified.txt b/identity-server/test/IdentityServer.IntegrationTests/Endpoints/Saml/SamlMetadataEndpointTests.metadata_endpoint_should_return_metadata.verified.txt new file mode 100644 index 000000000..c5bab218d --- /dev/null +++ b/identity-server/test/IdentityServer.IntegrationTests/Endpoints/Saml/SamlMetadataEndpointTests.metadata_endpoint_should_return_metadata.verified.txt @@ -0,0 +1 @@ +MIICojCCAYqgAwIBAgIIIjGqKDo3ME4wDQYJKoZIhvcNAQELBQAwETEPMA0GA1UEAxMGZm9vYmFyMB4XDTI1MTExMDE0MTEyNloXDTMwMTExMDE0MTEyNlowETEPMA0GA1UEAxMGZm9vYmFyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu/fcM55jlB810lyxGpgk0Zhw83Liqz80l3zLLAZgJ/IUdBx9VFD28BeO37eByHDXxBIQdHFYXQj+lv2g3KFRxVzfZhiFUrb1UydJYFZ951sQUEsP4T/Fpbyb95HNrwG2NwE5/fk1MXr9no4ydsQTZA6EWOfbxn6o2YQs/8QdDykhCzpZcWYbk5AKS/G6nYLpwuW4UsyMQ6ur9ZQXtwDS/hGyP3RjK8pjqkckbQG9ZapI+hWezIJkGmkXcuIx+FpZbdjjwu/SIcNNrBIXLbrbWyxoWt4y2jWfDixanBAubBLtx6tCg69trJ3M5gZkFZBR3CVqs78fYZUThKBTS20afQIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQCxB08tE2bDWpF5mR14kQvRUA/2hZKeC6CYYGEwOu1hbh5m3rVj4T9GPgOh+s6tX+rCb0IoV1uD9iSeTd3XaJ/1sSFkgVD/PaA6NRgzKVeDXLl9rZGAnOmp/Es3Pz35FbPxZKTe8UDyFHySbioLaLvtODhzX7SeGP3BcRpp8rZLvggMYiqo3w39+qZcgZPIBP4yRSulBYb3r9qagQ/n//gp7SmenCQmjA5L7pTn7QggFQsSQmB6dyNS54cUk0niUsTihT9oqpMnXmsXonXf5cv3tnaydreiB4aPea+OjjY3oy8hvHUH6FuQQX7t3RllZlPGJQFZe61rYMVmRRjlHWTAurn:oasis:names:tc:SAML:1.1:nameid-format:emailAddressurn:oasis:names:tc:SAML:2.0:nameid-format:persistenturn:oasis:names:tc:SAML:2.0:nameid-format:transienturn:oasis:names:tc:SAML:1.1:nameid-format:unspecified diff --git a/identity-server/test/IdentityServer.IntegrationTests/Endpoints/Saml/SamlSigninCallbackEndpointTests.cs b/identity-server/test/IdentityServer.IntegrationTests/Endpoints/Saml/SamlSigninCallbackEndpointTests.cs index f0dd04044..0e96a0cba 100644 --- a/identity-server/test/IdentityServer.IntegrationTests/Endpoints/Saml/SamlSigninCallbackEndpointTests.cs +++ b/identity-server/test/IdentityServer.IntegrationTests/Endpoints/Saml/SamlSigninCallbackEndpointTests.cs @@ -10,17 +10,16 @@ using Duende.IdentityModel; using Duende.IdentityServer.Internal.Saml.SingleSignin; using Duende.IdentityServer.Models; using Microsoft.AspNetCore.Mvc; -using Xunit.Abstractions; using static Duende.IdentityServer.IntegrationTests.Endpoints.Saml.SamlTestHelpers; using StateId = Duende.IdentityServer.Internal.Saml.SingleSignin.Models.StateId; namespace Duende.IdentityServer.IntegrationTests.Endpoints.Saml; -public class SamlSigninCallbackEndpointTests(ITestOutputHelper output) +public class SamlSigninCallbackEndpointTests { private const string Category = "SAML Signin Callback Endpoint"; - private SamlFixture Fixture = new(output); + private SamlFixture Fixture = new(); private SamlData Data => Fixture.Data; diff --git a/identity-server/test/IdentityServer.IntegrationTests/Endpoints/Saml/SamlSigninEndpointTests.cs b/identity-server/test/IdentityServer.IntegrationTests/Endpoints/Saml/SamlSigninEndpointTests.cs index d2d48e77b..2063a2772 100644 --- a/identity-server/test/IdentityServer.IntegrationTests/Endpoints/Saml/SamlSigninEndpointTests.cs +++ b/identity-server/test/IdentityServer.IntegrationTests/Endpoints/Saml/SamlSigninEndpointTests.cs @@ -15,19 +15,18 @@ using Duende.IdentityServer.Internal.Saml; using Duende.IdentityServer.Models; using Duende.IdentityServer.Saml.Models; using Microsoft.AspNetCore.Mvc; -using Xunit.Abstractions; using static Duende.IdentityServer.IntegrationTests.Endpoints.Saml.SamlTestHelpers; using SamlStatusCode = Duende.IdentityServer.Saml.Models.SamlStatusCode; namespace Duende.IdentityServer.IntegrationTests.Endpoints.Saml; -public class SamlSigninEndpointTests(ITestOutputHelper output) +public class SamlSigninEndpointTests { private const string Category = "SAML Signin Endpoint"; private readonly CancellationToken _ct = CancellationToken.None; - private SamlFixture Fixture = new(output); + private SamlFixture Fixture = new(); private SamlData Data => Fixture.Data; @@ -106,7 +105,7 @@ public class SamlSigninEndpointTests(ITestOutputHelper output) errorResponse.StatusCode.ShouldBe("urn:oasis:names:tc:SAML:2.0:status:VersionMismatch"); errorResponse.StatusMessage.ShouldNotBeNull(); errorResponse.StatusMessage.ShouldContain("Only Version 2.0 is supported"); - errorResponse.Issuer.ShouldBe(Fixture.Host!.Uri()); + errorResponse.Issuer.ShouldBe(Fixture.Host!.Url()); errorResponse.InResponseTo.ShouldNotBeNullOrEmpty(); errorResponse.AssertionConsumerServiceUrl.ShouldBe(Data.AcsUrl.ToString()); } @@ -223,7 +222,7 @@ public class SamlSigninEndpointTests(ITestOutputHelper output) errorResponse.StatusCode.ShouldBe("urn:oasis:names:tc:SAML:2.0:status:VersionMismatch"); errorResponse.StatusMessage.ShouldNotBeNull(); errorResponse.StatusMessage.ShouldContain("Only Version 2.0 is supported"); - errorResponse.Issuer.ShouldBe(Fixture.Host!.Uri()); + errorResponse.Issuer.ShouldBe(Fixture.Host!.Url()); errorResponse.InResponseTo.ShouldNotBeNullOrEmpty(); errorResponse.AssertionConsumerServiceUrl.ShouldBe(Data.AcsUrl.ToString()); } @@ -316,7 +315,7 @@ public class SamlSigninEndpointTests(ITestOutputHelper output) errorResponse.StatusCode.ShouldBe("urn:oasis:names:tc:SAML:2.0:status:Requester"); errorResponse.StatusMessage.ShouldBe("Request IssueInstant is in the future"); - errorResponse.Issuer.ShouldBe(Fixture.Host!.Uri()); + errorResponse.Issuer.ShouldBe(Fixture.Host!.Url()); errorResponse.InResponseTo.ShouldNotBeNullOrEmpty(); errorResponse.AssertionConsumerServiceUrl.ShouldBe(Data.AcsUrl.ToString()); } @@ -498,7 +497,7 @@ public class SamlSigninEndpointTests(ITestOutputHelper output) await Fixture.InitializeAsync(); // Use the correct Destination attribute - var correctDestination = new Uri(Fixture.Host!.Uri() + "/saml/signin"); + var correctDestination = new Uri(Fixture.Host!.Url() + "/saml/signin"); var authnRequestXml = Build.AuthNRequestXml(destination: correctDestination); var urlEncoded = await EncodeRequest(authnRequestXml); @@ -540,7 +539,7 @@ public class SamlSigninEndpointTests(ITestOutputHelper output) successResponse.ResponseId.ShouldNotBeNullOrEmpty(); successResponse.Destination.ShouldBe(Fixture.Data.AcsUrl.ToString()); successResponse.IssueInstant.ShouldBe(Data.FakeTimeProvider.GetUtcNow().ToString("yyyy-MM-ddTHH:mm:ss.fffZ")); - successResponse.Issuer.ShouldBe(Fixture.Host?.Uri()); + successResponse.Issuer.ShouldBe(Fixture.Host?.Url()); successResponse.InResponseTo.ShouldBe(Data.RequestId); successResponse.StatusCode.ShouldBe(SamlStatusCode.Success.Value); @@ -550,7 +549,7 @@ public class SamlSigninEndpointTests(ITestOutputHelper output) assertion.Id.ShouldNotBeNullOrEmpty(); assertion.Version.ShouldBe("2.0"); assertion.IssueInstant.ShouldBe(Data.FakeTimeProvider.GetUtcNow().ToString("yyyy-MM-ddTHH:mm:ss.fffZ")); - assertion.Issuer.ShouldBe(Fixture.Host?.Uri()); + assertion.Issuer.ShouldBe(Fixture.Host?.Url()); var subject = assertion.Subject; subject.ShouldNotBeNull(); @@ -789,7 +788,7 @@ public class SamlSigninEndpointTests(ITestOutputHelper output) errorResponse.StatusCode.ShouldBe("urn:oasis:names:tc:SAML:2.0:status:NoPassive"); errorResponse.StatusMessage.ShouldBe("The user is not currently logged in and passive login was requested."); - errorResponse.Issuer.ShouldBe(Fixture.Host!.Uri()); + errorResponse.Issuer.ShouldBe(Fixture.Host!.Url()); errorResponse.InResponseTo.ShouldNotBeNullOrEmpty(); errorResponse.AssertionConsumerServiceUrl.ShouldBe(Data.AcsUrl.ToString()); } @@ -814,7 +813,7 @@ public class SamlSigninEndpointTests(ITestOutputHelper output) errorResponse.StatusCode.ShouldBe("urn:oasis:names:tc:SAML:2.0:status:NoPassive"); errorResponse.StatusMessage.ShouldBe("The user is not currently logged in"); - errorResponse.Issuer.ShouldBe(Fixture.Host!.Uri()); + errorResponse.Issuer.ShouldBe(Fixture.Host!.Url()); errorResponse.InResponseTo.ShouldNotBeNullOrEmpty(); errorResponse.AssertionConsumerServiceUrl.ShouldBe(Data.AcsUrl.ToString()); } diff --git a/identity-server/test/IdentityServer.IntegrationTests/Endpoints/Saml/SamlSingleLogoutCallbackEndpointTests.cs b/identity-server/test/IdentityServer.IntegrationTests/Endpoints/Saml/SamlSingleLogoutCallbackEndpointTests.cs index 85512f57b..2192c6d6c 100644 --- a/identity-server/test/IdentityServer.IntegrationTests/Endpoints/Saml/SamlSingleLogoutCallbackEndpointTests.cs +++ b/identity-server/test/IdentityServer.IntegrationTests/Endpoints/Saml/SamlSingleLogoutCallbackEndpointTests.cs @@ -5,15 +5,14 @@ using System.Net; using Duende.IdentityServer.Models; using Duende.IdentityServer.Saml.Models; using Duende.IdentityServer.Stores; -using Xunit.Abstractions; namespace Duende.IdentityServer.IntegrationTests.Endpoints.Saml; -public class SamlSingleLogoutCallbackEndpointTests(ITestOutputHelper output) +public class SamlSingleLogoutCallbackEndpointTests { private const string Category = "SAML single logout callback endpoint"; - private SamlFixture Fixture = new(output); + private SamlFixture Fixture = new(); private SamlData Data => Fixture.Data; diff --git a/identity-server/test/IdentityServer.IntegrationTests/Endpoints/Saml/SamlSingleLogoutEndpointTests.cs b/identity-server/test/IdentityServer.IntegrationTests/Endpoints/Saml/SamlSingleLogoutEndpointTests.cs index 269feb50d..4a0e04644 100644 --- a/identity-server/test/IdentityServer.IntegrationTests/Endpoints/Saml/SamlSingleLogoutEndpointTests.cs +++ b/identity-server/test/IdentityServer.IntegrationTests/Endpoints/Saml/SamlSingleLogoutEndpointTests.cs @@ -10,16 +10,15 @@ using Duende.IdentityModel; using Duende.IdentityServer.Models; using Duende.IdentityServer.Saml.Models; using Microsoft.AspNetCore.Mvc; -using Xunit.Abstractions; using static Duende.IdentityServer.IntegrationTests.Endpoints.Saml.SamlTestHelpers; namespace Duende.IdentityServer.IntegrationTests.Endpoints.Saml; -public class SamlSingleLogoutEndpointTests(ITestOutputHelper output) +public class SamlSingleLogoutEndpointTests { private const string Category = "SAML single logout endpoint"; - private SamlFixture Fixture = new(output); + private SamlFixture Fixture = new(); private SamlData Data => Fixture.Data; @@ -151,7 +150,7 @@ public class SamlSingleLogoutEndpointTests(ITestOutputHelper output) await Fixture.Client.GetAsync("/__signin", CancellationToken.None); var logoutRequestXml = Build.LogoutRequestXml( - destination: new Uri($"{Fixture.Host!.Uri()}/saml/logout"), + destination: new Uri($"{Fixture.Host!.Url()}/saml/logout"), sessionIndex: "session123"); var urlEncoded = await EncodeAndSignRequest(logoutRequestXml, sp, CancellationToken.None); @@ -176,7 +175,7 @@ public class SamlSingleLogoutEndpointTests(ITestOutputHelper output) await Fixture.InitializeAsync(); var logoutRequestXml = Build.LogoutRequestXml( - destination: new Uri($"{Fixture.Host!.Uri()}/saml/logout"), + destination: new Uri($"{Fixture.Host!.Url()}/saml/logout"), version: "1.0"); var urlEncoded = await EncodeAndSignRequest(logoutRequestXml, sp, CancellationToken.None); @@ -200,7 +199,7 @@ public class SamlSingleLogoutEndpointTests(ITestOutputHelper output) var futureTime = Data.Now.AddMinutes(10); var logoutRequestXml = Build.LogoutRequestXml( - destination: new Uri($"{Fixture.Host!.Uri()}/saml/logout"), + destination: new Uri($"{Fixture.Host!.Url()}/saml/logout"), issueInstant: futureTime, sessionIndex: "session123"); var urlEncoded = await EncodeAndSignRequest(logoutRequestXml, sp, CancellationToken.None); @@ -226,7 +225,7 @@ public class SamlSingleLogoutEndpointTests(ITestOutputHelper output) var oldTime = Data.Now.AddMinutes(-10); var logoutRequestXml = Build.LogoutRequestXml( - destination: new Uri($"{Fixture.Host!.Uri()}/saml/logout"), + destination: new Uri($"{Fixture.Host!.Url()}/saml/logout"), issueInstant: oldTime, sessionIndex: "session123"); var urlEncoded = await EncodeAndSignRequest(logoutRequestXml, sp, CancellationToken.None); @@ -261,7 +260,7 @@ public class SamlSingleLogoutEndpointTests(ITestOutputHelper output) // Assert var logoutResponse = await ExtractSamlLogoutResponseFromPostAsync(result, CancellationToken.None); logoutResponse.StatusCode.ShouldBe(SamlStatusCode.Requester.Value); - logoutResponse.StatusMessage.ShouldBe($"Invalid destination. Expected '{Fixture.Host!.Uri()}/saml/logout'"); + logoutResponse.StatusMessage.ShouldBe($"Invalid destination. Expected '{Fixture.Host!.Url()}/saml/logout'"); } [Fact] @@ -275,7 +274,7 @@ public class SamlSingleLogoutEndpointTests(ITestOutputHelper output) await Fixture.InitializeAsync(); var logoutRequestXml = Build.LogoutRequestXml( - destination: new Uri($"{Fixture.Host!.Uri()}/saml/logout"), + destination: new Uri($"{Fixture.Host!.Url()}/saml/logout"), sessionIndex: "session123"); var urlEncoded = await EncodeRequest(logoutRequestXml, CancellationToken.None); @@ -301,7 +300,7 @@ public class SamlSingleLogoutEndpointTests(ITestOutputHelper output) // Create a logout request without a signature var logoutRequestXml = Build.LogoutRequestXml( - destination: new Uri($"{Fixture.Host!.Uri()}/saml/logout"), + destination: new Uri($"{Fixture.Host!.Url()}/saml/logout"), sessionIndex: "session123"); var urlEncoded = await EncodeRequest(logoutRequestXml, CancellationToken.None); @@ -352,7 +351,7 @@ public class SamlSingleLogoutEndpointTests(ITestOutputHelper output) // Don't sign in a user - no authenticated session var logoutRequestXml = Build.LogoutRequestXml( - destination: new Uri($"{Fixture.Host!.Uri()}/saml/logout"), + destination: new Uri($"{Fixture.Host!.Url()}/saml/logout"), sessionIndex: "session123"); var urlEncoded = await EncodeAndSignRequest(logoutRequestXml, sp, CancellationToken.None); @@ -385,7 +384,7 @@ public class SamlSingleLogoutEndpointTests(ITestOutputHelper output) // Use a different service provider than what was established var logoutRequestXml = Build.LogoutRequestXml( issuer: anotherSp.EntityId, // Use a different SP so session will not be found - destination: new Uri($"{Fixture.Host!.Uri()}/saml/logout")); + destination: new Uri($"{Fixture.Host!.Url()}/saml/logout")); var urlEncoded = await EncodeAndSignRequest(logoutRequestXml, sp, CancellationToken.None); // Act @@ -413,7 +412,7 @@ public class SamlSingleLogoutEndpointTests(ITestOutputHelper output) // Use a different session index than what was established var logoutRequestXml = Build.LogoutRequestXml( - destination: new Uri($"{Fixture.Host!.Uri()}/saml/logout"), + destination: new Uri($"{Fixture.Host!.Url()}/saml/logout"), sessionIndex: "wrong-session-index"); var urlEncoded = await EncodeAndSignRequest(logoutRequestXml, sp, CancellationToken.None); @@ -444,7 +443,7 @@ public class SamlSingleLogoutEndpointTests(ITestOutputHelper output) var sessionIndex = await PerformSigninAndExtractSessionIndex(sp); var logoutRequestXml = Build.LogoutRequestXml( - destination: new Uri($"{Fixture.Host!.Uri()}/saml/logout"), + destination: new Uri($"{Fixture.Host!.Url()}/saml/logout"), sessionIndex: sessionIndex); var urlEncoded = await EncodeAndSignRequest(logoutRequestXml, sp, CancellationToken.None); @@ -482,7 +481,7 @@ public class SamlSingleLogoutEndpointTests(ITestOutputHelper output) var sessionIndex = await PerformSigninAndExtractSessionIndex(sp); var logoutRequestXml = Build.LogoutRequestXml( - destination: new Uri($"{Fixture.Host!.Uri()}/saml/logout"), + destination: new Uri($"{Fixture.Host!.Url()}/saml/logout"), sessionIndex: sessionIndex); var urlEncoded = await EncodeAndSignRequest(logoutRequestXml, sp, CancellationToken.None); @@ -495,7 +494,7 @@ public class SamlSingleLogoutEndpointTests(ITestOutputHelper output) // Verify user can no longer access protected resource and is redirected to login var finalProtectedResourceResult = await Fixture.Client.GetAsync("__protected-resource", CancellationToken.None); finalProtectedResourceResult.StatusCode.ShouldBe(HttpStatusCode.OK); - finalProtectedResourceResult.RequestMessage?.RequestUri?.AbsoluteUri.ShouldStartWith($"{Fixture.Host!.Uri()}{Fixture.LoginUrl.ToString()}"); + finalProtectedResourceResult.RequestMessage?.RequestUri?.AbsoluteUri.ShouldStartWith($"{Fixture.Host!.Url()}{Fixture.LoginUrl.ToString()}"); } private static async Task EncodeAndSignRequest( diff --git a/identity-server/test/IdentityServer.IntegrationTests/Endpoints/Saml/SustainSysSamlTestFixture.cs b/identity-server/test/IdentityServer.IntegrationTests/Endpoints/Saml/SustainSysSamlTestFixture.cs index 70e0445f0..2ae4b0fbc 100644 --- a/identity-server/test/IdentityServer.IntegrationTests/Endpoints/Saml/SustainSysSamlTestFixture.cs +++ b/identity-server/test/IdentityServer.IntegrationTests/Endpoints/Saml/SustainSysSamlTestFixture.cs @@ -17,12 +17,11 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Time.Testing; using Sustainsys.Saml2.AspNetCore2; -using Xunit.Abstractions; using IdentityProvider = Sustainsys.Saml2.IdentityProvider; namespace Duende.IdentityServer.IntegrationTests.Endpoints.Saml; -internal class SustainSysSamlTestFixture(ITestOutputHelper output) : IAsyncLifetime +internal class SustainSysSamlTestFixture : IAsyncLifetime { public TestFramework.GenericHost? Host = null!; public HttpClient? BrowserClient = null!; @@ -30,7 +29,7 @@ internal class SustainSysSamlTestFixture(ITestOutputHelper output) : IAsyncLifet public Uri IdentityProviderLoginUri => new Uri(new Uri(_samlFixture.Host!.Url()), _samlFixture.LoginUrl); - private readonly SamlFixture _samlFixture = new(output); + private readonly SamlFixture _samlFixture = new(); private bool _shouldGenerateSigningCertificate; private bool _shouldRequireEncryptedAssertions; @@ -49,7 +48,7 @@ internal class SustainSysSamlTestFixture(ITestOutputHelper output) : IAsyncLifet public void RequireEncryptedAssertions() => _shouldRequireEncryptedAssertions = true; - public async Task InitializeAsync() + public async ValueTask InitializeAsync() { // need to use current time because the SustainSys library does not rely on an abstraction such // as TimeProvider and times need to be current @@ -152,7 +151,7 @@ internal class SustainSysSamlTestFixture(ITestOutputHelper output) : IAsyncLifet BrowserClient = Host.HttpClient; } - public async Task DisposeAsync() + public async ValueTask DisposeAsync() { if (Host != null) { diff --git a/identity-server/test/IdentityServer.IntegrationTests/Endpoints/Saml/SustainSysSigninTests.cs b/identity-server/test/IdentityServer.IntegrationTests/Endpoints/Saml/SustainSysSigninTests.cs index 2d4280983..70c8ea0b8 100644 --- a/identity-server/test/IdentityServer.IntegrationTests/Endpoints/Saml/SustainSysSigninTests.cs +++ b/identity-server/test/IdentityServer.IntegrationTests/Endpoints/Saml/SustainSysSigninTests.cs @@ -3,16 +3,15 @@ using System.Net; using System.Web; -using Xunit.Abstractions; using static Duende.IdentityServer.IntegrationTests.Endpoints.Saml.SamlTestHelpers; namespace Duende.IdentityServer.IntegrationTests.Endpoints.Saml; -public class SustainSysSigninTests(ITestOutputHelper output) +public class SustainSysSigninTests { private const string Category = "SustainSys SAML signin"; - private SustainSysSamlTestFixture Fixture = new(output); + private SustainSysSamlTestFixture Fixture = new(); [Fact] [Trait("Category", Category)] diff --git a/identity-server/test/IdentityServer.IntegrationTests/Endpoints/Saml/TestSamlClaimsMapper.cs b/identity-server/test/IdentityServer.IntegrationTests/Endpoints/Saml/TestSamlClaimsMapper.cs new file mode 100644 index 000000000..38359e79c --- /dev/null +++ b/identity-server/test/IdentityServer.IntegrationTests/Endpoints/Saml/TestSamlClaimsMapper.cs @@ -0,0 +1,28 @@ +// Copyright (c) Duende Software. All rights reserved. +// See LICENSE in the project root for license information. + +using Duende.IdentityServer.Saml; +using Duende.IdentityServer.Saml.Models; + +namespace Duende.IdentityServer.IntegrationTests.Endpoints.Saml; + +/// +/// Test implementation of ISamlClaimsMapper that returns a single custom attribute. +/// Used by both unit and integration tests. +/// +public class TestSamlClaimsMapper : ISamlClaimsMapper +{ + public Task> MapClaimsAsync(SamlClaimsMappingContext mappingContext) + { + var attributes = new List + { + new() + { + Name = "CUSTOM_MAPPED", + NameFormat = "urn:oasis:names:tc:SAML:2.0:attrname-format:basic", + Values = new List { "custom_value" } + } + }; + return Task.FromResult>(attributes); + } +} diff --git a/identity-server/test/IdentityServer.IntegrationTests/IdentityServer.IntegrationTests.csproj b/identity-server/test/IdentityServer.IntegrationTests/IdentityServer.IntegrationTests.csproj index 321b0860b..967b114b2 100644 --- a/identity-server/test/IdentityServer.IntegrationTests/IdentityServer.IntegrationTests.csproj +++ b/identity-server/test/IdentityServer.IntegrationTests/IdentityServer.IntegrationTests.csproj @@ -18,6 +18,7 @@ + diff --git a/identity-server/test/IdentityServer.UnitTests/Saml/InMemoryTestServiceProviderStore.cs b/identity-server/test/IdentityServer.UnitTests/Saml/InMemoryTestServiceProviderStore.cs deleted file mode 100644 index a8d924cc8..000000000 --- a/identity-server/test/IdentityServer.UnitTests/Saml/InMemoryTestServiceProviderStore.cs +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) Duende Software. All rights reserved. -// See LICENSE in the project root for license information. - -#nullable enable -using Duende.IdentityServer.Models; -using Duende.IdentityServer.Stores; - -namespace UnitTests.Saml; - -/// -/// Simple in-memory implementation of ISamlServiceProviderStore for unit testing. -/// -internal class InMemoryTestServiceProviderStore : ISamlServiceProviderStore -{ - private readonly List _providers = []; - - public InMemoryTestServiceProviderStore() { } - public InMemoryTestServiceProviderStore(params SamlServiceProvider[] providers) => _providers.AddRange(providers); - - public void Add(SamlServiceProvider provider) => _providers.Add(provider); - - public Task FindByEntityIdAsync(string entityId) - => Task.FromResult(_providers.FirstOrDefault(p => p.EntityId == entityId)); -} diff --git a/identity-server/test/IdentityServer.UnitTests/Saml/SamlFrontChannelLogoutRequestBuilderTests.cs b/identity-server/test/IdentityServer.UnitTests/Saml/SamlFrontChannelLogoutRequestBuilderTests.cs index 43318c9cc..f427c5b18 100644 --- a/identity-server/test/IdentityServer.UnitTests/Saml/SamlFrontChannelLogoutRequestBuilderTests.cs +++ b/identity-server/test/IdentityServer.UnitTests/Saml/SamlFrontChannelLogoutRequestBuilderTests.cs @@ -12,7 +12,6 @@ using Duende.IdentityServer.Internal.Saml.SingleLogout; using Duende.IdentityServer.Models; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Time.Testing; -using UnitTests.Common; namespace UnitTests.Saml; diff --git a/identity-server/test/IdentityServer.UnitTests/Saml/SamlLogoutCallbackProcessorTests.cs b/identity-server/test/IdentityServer.UnitTests/Saml/SamlLogoutCallbackProcessorTests.cs index 8e7b9f5f6..9255e9adf 100644 --- a/identity-server/test/IdentityServer.UnitTests/Saml/SamlLogoutCallbackProcessorTests.cs +++ b/identity-server/test/IdentityServer.UnitTests/Saml/SamlLogoutCallbackProcessorTests.cs @@ -9,7 +9,6 @@ using Duende.IdentityServer.Services; using Duende.IdentityServer.Stores; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Time.Testing; -using UnitTests.Common; namespace UnitTests.Saml; diff --git a/identity-server/test/IdentityServer.UnitTests/Saml/SamlLogoutNotificationServiceTests.cs b/identity-server/test/IdentityServer.UnitTests/Saml/SamlLogoutNotificationServiceTests.cs index 466293c02..7d84d658d 100644 --- a/identity-server/test/IdentityServer.UnitTests/Saml/SamlLogoutNotificationServiceTests.cs +++ b/identity-server/test/IdentityServer.UnitTests/Saml/SamlLogoutNotificationServiceTests.cs @@ -7,6 +7,7 @@ using Duende.IdentityServer.Internal.Saml.Infrastructure; using Duende.IdentityServer.Internal.Saml.SingleLogout; using Duende.IdentityServer.Models; using Duende.IdentityServer.Saml.Models; +using Duende.IdentityServer.Stores; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Time.Testing; using UnitTests.Common; @@ -30,7 +31,7 @@ public class SamlLogoutNotificationServiceTests return new SamlLogoutNotificationService( _issuerNameService, - new InMemoryTestServiceProviderStore(samlServiceProviders), + new InMemorySamlServiceProviderStore(samlServiceProviders), frontChannelLogoutRequestBuilder, NullLogger.Instance); } diff --git a/products.slnx b/products.slnx index bd7db6cfd..0740d8b8d 100644 --- a/products.slnx +++ b/products.slnx @@ -165,6 +165,7 @@ +