Do not enable SAML by default, but provide mechnism to opt-in

This commit is contained in:
Brett Hazen 2026-02-20 14:54:46 -06:00
parent 64bda6c985
commit ca9e3a360f
14 changed files with 239 additions and 101 deletions

View file

@ -17,12 +17,6 @@ using Duende.IdentityServer.Hosting.DynamicProviders;
using Duende.IdentityServer.Hosting.FederatedSignOut;
using Duende.IdentityServer.Internal;
using Duende.IdentityServer.Internal.Saml;
using Duende.IdentityServer.Internal.Saml.Infrastructure;
using Duende.IdentityServer.Internal.Saml.Metadata;
using Duende.IdentityServer.Internal.Saml.SingleLogout;
using Duende.IdentityServer.Internal.Saml.SingleLogout.Models;
using Duende.IdentityServer.Internal.Saml.SingleSignin;
using Duende.IdentityServer.Internal.Saml.SingleSignin.Models;
using Duende.IdentityServer.Licensing;
using Duende.IdentityServer.Licensing.V2;
using Duende.IdentityServer.Licensing.V2.Diagnostics;
@ -135,14 +129,6 @@ public static class IdentityServerBuilderExtensionsCore
builder.AddEndpoint<TokenEndpoint>(EndpointNames.Token, ProtocolRoutePaths.Token.EnsureLeadingSlash());
builder.AddEndpoint<UserInfoEndpoint>(EndpointNames.UserInfo, ProtocolRoutePaths.UserInfo.EnsureLeadingSlash());
// SAML 2.0 endpoints
builder.AddEndpoint<SamlMetaDataEndpoint>(EndpointNames.SamlMetadata, ProtocolRoutePaths.SamlMetadata.EnsureLeadingSlash());
builder.AddEndpoint<SamlSigninEndpoint>(EndpointNames.SamlSignin, ProtocolRoutePaths.SamlSignin.EnsureLeadingSlash());
builder.AddEndpoint<SamlSigninCallbackEndpoint>(EndpointNames.SamlSigninCallback, ProtocolRoutePaths.SamlSigninCallback.EnsureLeadingSlash());
builder.AddEndpoint<SamlIdpInitiatedEndpoint>(EndpointNames.SamlIdpInitiated, ProtocolRoutePaths.SamlIdpInitiated.EnsureLeadingSlash());
builder.AddEndpoint<SamlSingleLogoutEndpoint>(EndpointNames.SamlLogout, ProtocolRoutePaths.SamlLogout.EnsureLeadingSlash());
builder.AddEndpoint<SamlSingleLogoutCallbackEndpoint>(EndpointNames.SamlLogoutCallback, ProtocolRoutePaths.SamlLogoutCallback.EnsureLeadingSlash());
builder.AddHttpWriter<AuthorizeInteractionPageResult, AuthorizeInteractionPageHttpWriter>();
builder.AddHttpWriter<AuthorizeResult, AuthorizeHttpWriter>();
builder.AddHttpWriter<BackchannelAuthenticationResult, BackchannelAuthenticationHttpWriter>();
@ -166,67 +152,6 @@ public static class IdentityServerBuilderExtensionsCore
return builder;
}
/// <summary>
/// Adds SAML 2.0 protocol services.
/// </summary>
/// <param name="builder">The builder.</param>
/// <returns></returns>
public static IIdentityServerBuilder AddSamlServices(this IIdentityServerBuilder builder)
{
// Serializers (Transient)
builder.Services.AddTransient<ISamlResultSerializer<SamlErrorResponse>, SamlErrorResponseXmlSerializer>();
builder.Services.AddTransient<ISamlResultSerializer<SamlResponse>, SamlResponse.Serializer>();
builder.Services.AddTransient<ISamlResultSerializer<LogoutResponse>, LogoutResponse.Serializer>();
// HTTP response writers
builder.AddHttpWriter<SamlErrorResponse, SamlErrorResponse.ResponseWriter>();
builder.AddHttpWriter<SamlResponse, SamlResponse.ResponseWriter>();
builder.AddHttpWriter<LogoutResponse, LogoutResponse.ResponseWriter>();
// Processors (Scoped)
builder.Services.AddScoped<SamlSigninRequestProcessor>();
builder.Services.AddScoped<SamlSigninCallbackRequestProcessor>();
builder.Services.AddScoped<SamlIdpInitiatedRequestProcessor>();
builder.Services.AddScoped<SamlLogoutRequestProcessor>();
builder.Services.AddScoped<SamlLogoutCallbackProcessor>();
// Builders (Scoped)
builder.Services.AddScoped<SamlResponseBuilder>();
builder.Services.AddScoped<LogoutResponseBuilder>();
builder.Services.AddScoped<SamlFrontChannelLogoutRequestBuilder>();
// Parsers / Extractors (Scoped)
builder.Services.AddScoped<AuthNRequestParser>();
builder.Services.AddScoped<LogoutRequestParser>();
builder.Services.AddScoped<SamlSigninRequestExtractor>();
builder.Services.AddScoped<SamlLogoutRequestExtractor>();
// Infrastructure (Scoped)
builder.Services.AddScoped<SamlUrlBuilder>();
builder.Services.AddScoped<SamlClaimsService>();
builder.Services.AddScoped<SamlNameIdGenerator>();
builder.Services.AddScoped<SamlResponseSigner>();
builder.Services.AddScoped<SamlProtocolMessageSigner>();
builder.Services.AddScoped<SamlAssertionEncryptor>();
builder.Services.AddScoped<SamlRequestValidator>();
builder.Services.TryAddScoped(typeof(SamlRequestSignatureValidator<,>));
// Interface → Implementation (TryAddScoped for extensibility)
builder.Services.TryAddScoped<ISamlSigninInteractionResponseGenerator, DefaultSamlSigninInteractionResponseGenerator>();
builder.Services.TryAddScoped<ISamlSigningService, SamlSigningService>();
builder.Services.TryAddScoped<ISamlLogoutNotificationService, SamlLogoutNotificationService>();
builder.Services.TryAddScoped<ISamlInteractionService, DefaultSamlInteractionService>();
// State management (Singleton)
builder.Services.TryAddSingleton<SamlSigninStateIdCookie>();
builder.Services.TryAddSingleton<ISamlSigninStateStore, DistributedCacheSamlSigninStateStore>();
// Default no-op service provider store (can be overridden by user)
builder.Services.TryAddTransient<ISamlServiceProviderStore, InMemorySamlServiceProviderStore>();
return builder;
}
/// <summary>
/// Adds an endpoint.
/// </summary>
@ -287,6 +212,12 @@ public static class IdentityServerBuilderExtensionsCore
builder.Services.TryAddTransient<IBackchannelAuthenticationUserValidator, NopBackchannelAuthenticationUserValidator>();
// Register no-op SAML services for services used in logout paths
// These are replaced by actual implementations in AddSaml and ISamlServiceProviderStore
// can be replaced with a call to AddSamlServiceProviderStore
builder.Services.TryAddTransient<ISamlServiceProviderStore, EmptySamlServiceProviderStore>();
builder.Services.TryAddScoped<ISamlLogoutNotificationService, NopSamlLogoutNotificationService>();
builder.Services.TryAddSingleton(typeof(IConcurrencyLock<>), typeof(DefaultConcurrencyLock<>));
builder.Services.TryAddTransient<IClientStore, EmptyClientStore>();

View file

@ -190,4 +190,17 @@ public static class IdentityServerBuilderExtensionsInMemory
builder.Services.TryAddSingleton<IPushedAuthorizationRequestStore, InMemoryPushedAuthorizationRequestStore>();
return builder;
}
/// <summary>
/// Adds the in-memory SAML service provider store.
/// </summary>
/// <param name="builder">The builder.</param>
/// <param name="serviceProviders">The SAML service providers.</param>
/// <returns></returns>
public static IIdentityServerBuilder AddInMemorySamlServiceProviders(this IIdentityServerBuilder builder, IEnumerable<SamlServiceProvider> serviceProviders)
{
builder.Services.AddSingleton(serviceProviders);
builder.AddSamlServiceProviderStore<InMemorySamlServiceProviderStore>();
return builder;
}
}

View file

@ -0,0 +1,134 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.
#nullable enable
using Duende.IdentityServer.Configuration;
using Duende.IdentityServer.Extensions;
using Duende.IdentityServer.Internal.Saml;
using Duende.IdentityServer.Internal.Saml.Infrastructure;
using Duende.IdentityServer.Internal.Saml.Metadata;
using Duende.IdentityServer.Internal.Saml.SingleLogout;
using Duende.IdentityServer.Internal.Saml.SingleLogout.Models;
using Duende.IdentityServer.Internal.Saml.SingleSignin;
using Duende.IdentityServer.Internal.Saml.SingleSignin.Models;
using Duende.IdentityServer.Saml;
using Duende.IdentityServer.Stores;
using Microsoft.Extensions.DependencyInjection.Extensions;
using static Duende.IdentityServer.IdentityServerConstants;
namespace Microsoft.Extensions.DependencyInjection;
/// <summary>
/// Builder extension methods for opting in to SAML 2.0 support.
/// </summary>
public static class IdentityServerBuilderExtensionsSaml
{
/// <summary>
/// Adds SAML 2.0 support to IdentityServer.
/// </summary>
/// <remarks>
/// Registers all SAML services and endpoints, and enables the SAML endpoints
/// in <see cref="EndpointsOptions"/>. The IdP-initiated SSO endpoint is not
/// enabled by default; set <see cref="EndpointsOptions.EnableSamlIdpInitiatedEndpoint"/>
/// to <c>true</c> explicitly if you need it.
/// </remarks>
/// <param name="builder">The builder.</param>
/// <returns></returns>
public static IIdentityServerBuilder AddSaml(this IIdentityServerBuilder builder)
{
builder.AddSamlServices();
builder.Services.Configure<IdentityServerOptions>(options =>
{
options.Endpoints.EnableSamlMetadataEndpoint = true;
options.Endpoints.EnableSamlSigninEndpoint = true;
options.Endpoints.EnableSamlSigninCallbackEndpoint = true;
options.Endpoints.EnableSamlLogoutEndpoint = true;
options.Endpoints.EnableSamlLogoutCallbackEndpoint = true;
// EnableSamlIdpInitiatedEndpoint intentionally left false — requires explicit opt-in.
});
return builder;
}
/// <summary>
/// Adds SAML 2.0 protocol services.
/// </summary>
/// <param name="builder">The builder.</param>
/// <returns></returns>
private static IIdentityServerBuilder AddSamlServices(this IIdentityServerBuilder builder)
{
// SAML 2.0 endpoints
builder.AddEndpoint<SamlMetaDataEndpoint>(EndpointNames.SamlMetadata, ProtocolRoutePaths.SamlMetadata.EnsureLeadingSlash());
builder.AddEndpoint<SamlSigninEndpoint>(EndpointNames.SamlSignin, ProtocolRoutePaths.SamlSignin.EnsureLeadingSlash());
builder.AddEndpoint<SamlSigninCallbackEndpoint>(EndpointNames.SamlSigninCallback, ProtocolRoutePaths.SamlSigninCallback.EnsureLeadingSlash());
builder.AddEndpoint<SamlIdpInitiatedEndpoint>(EndpointNames.SamlIdpInitiated, ProtocolRoutePaths.SamlIdpInitiated.EnsureLeadingSlash());
builder.AddEndpoint<SamlSingleLogoutEndpoint>(EndpointNames.SamlLogout, ProtocolRoutePaths.SamlLogout.EnsureLeadingSlash());
builder.AddEndpoint<SamlSingleLogoutCallbackEndpoint>(EndpointNames.SamlLogoutCallback, ProtocolRoutePaths.SamlLogoutCallback.EnsureLeadingSlash());
// Serializers (Transient)
builder.Services.AddTransient<ISamlResultSerializer<SamlErrorResponse>, SamlErrorResponseXmlSerializer>();
builder.Services.AddTransient<ISamlResultSerializer<SamlResponse>, SamlResponse.Serializer>();
builder.Services.AddTransient<ISamlResultSerializer<LogoutResponse>, LogoutResponse.Serializer>();
// HTTP response writers
builder.AddHttpWriter<SamlErrorResponse, SamlErrorResponse.ResponseWriter>();
builder.AddHttpWriter<SamlResponse, SamlResponse.ResponseWriter>();
builder.AddHttpWriter<LogoutResponse, LogoutResponse.ResponseWriter>();
// Processors (Scoped)
builder.Services.AddScoped<SamlSigninRequestProcessor>();
builder.Services.AddScoped<SamlSigninCallbackRequestProcessor>();
builder.Services.AddScoped<SamlIdpInitiatedRequestProcessor>();
builder.Services.AddScoped<SamlLogoutRequestProcessor>();
builder.Services.AddScoped<SamlLogoutCallbackProcessor>();
// Builders (Scoped)
builder.Services.AddScoped<SamlResponseBuilder>();
builder.Services.AddScoped<LogoutResponseBuilder>();
builder.Services.AddScoped<SamlFrontChannelLogoutRequestBuilder>();
// Parsers / Extractors (Scoped)
builder.Services.AddScoped<AuthNRequestParser>();
builder.Services.AddScoped<LogoutRequestParser>();
builder.Services.AddScoped<SamlSigninRequestExtractor>();
builder.Services.AddScoped<SamlLogoutRequestExtractor>();
// Infrastructure (Scoped)
builder.Services.AddScoped<SamlUrlBuilder>();
builder.Services.AddScoped<SamlClaimsService>();
builder.Services.AddScoped<SamlNameIdGenerator>();
builder.Services.AddScoped<SamlResponseSigner>();
builder.Services.AddScoped<SamlProtocolMessageSigner>();
builder.Services.AddScoped<SamlAssertionEncryptor>();
builder.Services.AddScoped<SamlRequestValidator>();
builder.Services.TryAddScoped(typeof(SamlRequestSignatureValidator<,>));
// Interface → Implementation (TryAddScoped for extensibility)
builder.Services.TryAddScoped<ISamlSigninInteractionResponseGenerator, DefaultSamlSigninInteractionResponseGenerator>();
builder.Services.TryAddScoped<ISamlSigningService, SamlSigningService>();
// Replace the no-op registered by AddCoreServices with the real implementation.
builder.Services.Replace(ServiceDescriptor.Scoped<ISamlLogoutNotificationService, SamlLogoutNotificationService>());
builder.Services.TryAddScoped<ISamlInteractionService, DefaultSamlInteractionService>();
// State management (Singleton)
builder.Services.TryAddSingleton<SamlSigninStateIdCookie>();
builder.Services.TryAddSingleton<ISamlSigninStateStore, DistributedCacheSamlSigninStateStore>();
return builder;
}
/// <summary>
/// Adds a custom SAML service provider store.
/// </summary>
/// <typeparam name="T">The type of the <see cref="ISamlServiceProviderStore"/> implementation.</typeparam>
/// <param name="builder">The builder.</param>
/// <returns></returns>
public static IIdentityServerBuilder AddSamlServiceProviderStore<T>(this IIdentityServerBuilder builder)
where T : class, ISamlServiceProviderStore
{
builder.Services.AddTransient<ISamlServiceProviderStore, T>();
return builder;
}
}

View file

@ -37,7 +37,6 @@ public static class IdentityServerServiceCollectionExtensions
.AddCookieAuthentication()
.AddCoreServices()
.AddDefaultEndpoints()
.AddSamlServices()
.AddPluggableServices()
.AddKeyManagement()
.AddDynamicProvidersCore()

View file

@ -118,7 +118,7 @@ public class EndpointsOptions
/// <value>
/// <c>true</c> if the SAML metadata endpoint is enabled; otherwise, <c>false</c>.
/// </value>
public bool EnableSamlMetadataEndpoint { get; set; } = true;
public bool EnableSamlMetadataEndpoint { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the SAML sign-in (SSO) endpoint is enabled.
@ -126,7 +126,7 @@ public class EndpointsOptions
/// <value>
/// <c>true</c> if the SAML sign-in endpoint is enabled; otherwise, <c>false</c>.
/// </value>
public bool EnableSamlSigninEndpoint { get; set; } = true;
public bool EnableSamlSigninEndpoint { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the SAML sign-in callback endpoint is enabled.
@ -134,7 +134,7 @@ public class EndpointsOptions
/// <value>
/// <c>true</c> if the SAML sign-in callback endpoint is enabled; otherwise, <c>false</c>.
/// </value>
public bool EnableSamlSigninCallbackEndpoint { get; set; } = true;
public bool EnableSamlSigninCallbackEndpoint { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the SAML IdP-initiated SSO endpoint is enabled.
@ -142,7 +142,7 @@ public class EndpointsOptions
/// <value>
/// <c>true</c> if the SAML IdP-initiated endpoint is enabled; otherwise, <c>false</c>.
/// </value>
public bool EnableSamlIdpInitiatedEndpoint { get; set; } = true;
public bool EnableSamlIdpInitiatedEndpoint { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the SAML Single Logout (SLO) endpoint is enabled.
@ -150,7 +150,7 @@ public class EndpointsOptions
/// <value>
/// <c>true</c> if the SAML logout endpoint is enabled; otherwise, <c>false</c>.
/// </value>
public bool EnableSamlLogoutEndpoint { get; set; } = true;
public bool EnableSamlLogoutEndpoint { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the SAML Single Logout callback endpoint is enabled.
@ -158,5 +158,5 @@ public class EndpointsOptions
/// <value>
/// <c>true</c> if the SAML logout callback endpoint is enabled; otherwise, <c>false</c>.
/// </value>
public bool EnableSamlLogoutCallbackEndpoint { get; set; } = true;
public bool EnableSamlLogoutCallbackEndpoint { get; set; }
}

View file

@ -0,0 +1,12 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.
using Duende.IdentityServer.Models;
using Duende.IdentityServer.Stores;
namespace Duende.IdentityServer.Internal.Saml;
internal class EmptySamlServiceProviderStore : ISamlServiceProviderStore
{
public Task<SamlServiceProvider> FindByEntityIdAsync(string entityId) => Task.FromResult<SamlServiceProvider>(null);
}

View file

@ -2,8 +2,6 @@
// See LICENSE in the project root for license information.
#nullable enable
using Duende.IdentityServer.Saml.Models;
namespace Duende.IdentityServer.Internal.Saml.Infrastructure;
/// <summary>

View file

@ -0,0 +1,15 @@
// 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.Saml;
namespace Duende.IdentityServer.Internal.Saml;
internal class NopSamlLogoutNotificationService : ISamlLogoutNotificationService
{
public Task<IEnumerable<ISamlFrontChannelLogout>> GetSamlFrontChannelLogoutsAsync(LogoutNotificationContext context) =>
Task.FromResult(Enumerable.Empty<ISamlFrontChannelLogout>());
}

View file

@ -11,7 +11,6 @@ using Duende.IdentityServer.Internal.Saml.SingleLogout.Models;
using Duende.IdentityServer.Internal.Saml.SingleSignin.Models;
using Duende.IdentityServer.Models;
using Duende.IdentityServer.Saml;
using Duende.IdentityServer.Saml.Models;
using LogoutRequest = Duende.IdentityServer.Internal.Saml.SingleLogout.Models.LogoutRequest;
namespace Duende.IdentityServer.Internal.Saml.SingleLogout;

View file

@ -8,7 +8,6 @@ using Duende.IdentityServer.Hosting.DynamicProviders;
using Duende.IdentityServer.Internal;
using Duende.IdentityServer.Internal.Saml;
using Duende.IdentityServer.Internal.Saml.SingleLogout;
using Duende.IdentityServer.Internal.Saml.SingleSignin;
using Duende.IdentityServer.ResponseHandling;
using Duende.IdentityServer.Saml;
using Duende.IdentityServer.Services;
@ -60,9 +59,9 @@ internal class RegisteredImplementationsDiagnosticEntry(ServiceCollectionAccesso
"SAML", [
new(typeof(ISamlClaimsMapper), []),
new(typeof(ISamlFrontChannelLogout), [typeof(SamlHttpPostFrontChannelLogout), typeof(SamlHttpRedirectFrontChannelLogout)]),
new(typeof(ISamlInteractionService),[typeof(DefaultSamlInteractionService)]),
new(typeof(ISamlLogoutNotificationService), [typeof(SamlLogoutNotificationService)]),
new(typeof(ISamlSigninInteractionResponseGenerator),[typeof(DefaultSamlSigninInteractionResponseGenerator)]),
new(typeof(ISamlInteractionService),[]),
new(typeof(ISamlLogoutNotificationService), [typeof(NopSamlLogoutNotificationService)]),
new(typeof(ISamlSigninInteractionResponseGenerator),[]),
]
},
{
@ -96,7 +95,6 @@ internal class RegisteredImplementationsDiagnosticEntry(ServiceCollectionAccesso
new(typeof(IRefreshTokenService), [typeof(DefaultRefreshTokenService)]),
new(typeof(IReplayCache), [typeof(DefaultReplayCache)]),
new(typeof(IReturnUrlParser), [typeof(OidcReturnUrlParser)]),
new(typeof(ISamlServiceProviderStore), [typeof(InMemorySamlServiceProviderStore)]),
new(typeof(IServerUrls), [typeof(DefaultServerUrls)]),
new(typeof(ISessionCoordinationService), [typeof(DefaultSessionCoordinationService)]),
new(typeof(ISessionManagementService), []),
@ -126,6 +124,7 @@ internal class RegisteredImplementationsDiagnosticEntry(ServiceCollectionAccesso
new(typeof(IReferenceTokenStore), [typeof(DefaultReferenceTokenStore)]),
new(typeof(IRefreshTokenStore), [typeof(DefaultRefreshTokenStore)]),
new(typeof(IResourceStore), [typeof(EmptyResourceStore)]),
new(typeof(ISamlServiceProviderStore), [typeof(EmptySamlServiceProviderStore)]),
new(typeof(IServerSideSessionsMarker), []),
new(typeof(IServerSideSessionStore),[]),
new(typeof(IServerSideTicketStore),[]),

View file

@ -9,8 +9,8 @@ using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using Duende.IdentityModel;
using Duende.IdentityServer.Models;
using static Duende.IdentityServer.IntegrationTests.Endpoints.Saml.SamlTestHelpers;
using Duende.IdentityServer.Saml.Models;
using static Duende.IdentityServer.IntegrationTests.Endpoints.Saml.SamlTestHelpers;
namespace Duende.IdentityServer.IntegrationTests.Endpoints.Saml;

View file

@ -121,9 +121,6 @@ internal class SamlFixture : IAsyncLifetime
// Configure SAML options
services.Configure(ConfigureSamlOptions);
// Register in-memory SAML service provider store with our service providers
services.AddSingleton<ISamlServiceProviderStore>(new InMemorySamlServiceProviderStore(_serviceProviders));
// Replace the developer signing credential with our X509 certificate
// Remove the ISigningCredentialStore registration added by AddDeveloperSigningCredential
var signingCredentialDescriptor = services.FirstOrDefault(d => d.ServiceType == typeof(ISigningCredentialStore));
@ -136,7 +133,8 @@ internal class SamlFixture : IAsyncLifetime
services.AddIdentityServerBuilder()
.AddSigningCredential(selfSignedCertificate)
.AddProfileService<DefaultProfileService>()
.AddSamlServices();
.AddSaml()
.AddInMemorySamlServiceProviders(_serviceProviders);
ConfigureServices(services);

View file

@ -30,6 +30,10 @@ public class SamlIdpInitiatedEndpointTests
var sp = Build.SamlServiceProvider();
sp.AllowIdpInitiated = true;
Fixture.ServiceProviders.Add(sp);
Fixture.ConfigureIdentityServerOptions = options =>
{
options.Endpoints.EnableSamlIdpInitiatedEndpoint = true;
};
await Fixture.InitializeAsync();
Fixture.UserToSignIn =
@ -55,6 +59,10 @@ public class SamlIdpInitiatedEndpointTests
var sp = Build.SamlServiceProvider();
sp.AllowIdpInitiated = true;
Fixture.ServiceProviders.Add(sp);
Fixture.ConfigureIdentityServerOptions = options =>
{
options.Endpoints.EnableSamlIdpInitiatedEndpoint = true;
};
await Fixture.InitializeAsync();
Fixture.UserToSignIn =
@ -87,6 +95,10 @@ public class SamlIdpInitiatedEndpointTests
var sp = Build.SamlServiceProvider();
sp.AllowIdpInitiated = true;
Fixture.ServiceProviders.Add(sp);
Fixture.ConfigureIdentityServerOptions = options =>
{
options.Endpoints.EnableSamlIdpInitiatedEndpoint = true;
};
await Fixture.InitializeAsync();
// Act
@ -106,6 +118,10 @@ public class SamlIdpInitiatedEndpointTests
{
// Arrange
Fixture.ServiceProviders.Add(Build.SamlServiceProvider());
Fixture.ConfigureIdentityServerOptions = options =>
{
options.Endpoints.EnableSamlIdpInitiatedEndpoint = true;
};
await Fixture.InitializeAsync();
// Act
@ -130,6 +146,10 @@ public class SamlIdpInitiatedEndpointTests
sp.Enabled = false;
sp.AllowIdpInitiated = true;
Fixture.ServiceProviders.Add(sp);
Fixture.ConfigureIdentityServerOptions = options =>
{
options.Endpoints.EnableSamlIdpInitiatedEndpoint = true;
};
await Fixture.InitializeAsync();
// Act
@ -152,6 +172,10 @@ public class SamlIdpInitiatedEndpointTests
var sp = Build.SamlServiceProvider();
sp.AllowIdpInitiated = false;
Fixture.ServiceProviders.Add(sp);
Fixture.ConfigureIdentityServerOptions = options =>
{
options.Endpoints.EnableSamlIdpInitiatedEndpoint = true;
};
await Fixture.InitializeAsync();
// Act
@ -178,6 +202,10 @@ public class SamlIdpInitiatedEndpointTests
var sp = Build.SamlServiceProvider();
sp.AllowIdpInitiated = true;
Fixture.ServiceProviders.Add(sp);
Fixture.ConfigureIdentityServerOptions = options =>
{
options.Endpoints.EnableSamlIdpInitiatedEndpoint = true;
};
await Fixture.InitializeAsync();
// Act
@ -202,6 +230,10 @@ public class SamlIdpInitiatedEndpointTests
sp.AllowIdpInitiated = true;
sp.AssertionConsumerServiceUrls = Array.Empty<Uri>();
Fixture.ServiceProviders.Add(sp);
Fixture.ConfigureIdentityServerOptions = options =>
{
options.Endpoints.EnableSamlIdpInitiatedEndpoint = true;
};
await Fixture.InitializeAsync();
// Act
@ -227,6 +259,10 @@ public class SamlIdpInitiatedEndpointTests
sp.AllowIdpInitiated = true;
sp.AssertionConsumerServiceUrls = [firstAcsUrl, secondAcsUrl];
Fixture.ServiceProviders.Add(sp);
Fixture.ConfigureIdentityServerOptions = options =>
{
options.Endpoints.EnableSamlIdpInitiatedEndpoint = true;
};
await Fixture.InitializeAsync();
Fixture.UserToSignIn =
@ -257,6 +293,10 @@ public class SamlIdpInitiatedEndpointTests
var sp = Build.SamlServiceProvider();
sp.AllowIdpInitiated = true;
Fixture.ServiceProviders.Add(sp);
Fixture.ConfigureIdentityServerOptions = options =>
{
options.Endpoints.EnableSamlIdpInitiatedEndpoint = true;
};
await Fixture.InitializeAsync();
Fixture.UserToSignIn =
@ -332,6 +372,10 @@ public class SamlIdpInitiatedEndpointTests
public async Task missing_sp_entity_id_parameter_returns_bad_request()
{
Fixture.ServiceProviders.Add(Build.SamlServiceProvider());
Fixture.ConfigureIdentityServerOptions = options =>
{
options.Endpoints.EnableSamlIdpInitiatedEndpoint = true;
};
await Fixture.InitializeAsync();
var result = await Fixture.Client.GetAsync("/saml/idp-initiated", CancellationToken.None);

View file

@ -9,7 +9,6 @@ using Duende.IdentityModel;
using Duende.IdentityServer.IntegrationTests.Common;
using Duende.IdentityServer.IntegrationTests.TestFramework;
using Duende.IdentityServer.Models;
using Duende.IdentityServer.Stores;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Builder;
@ -95,10 +94,6 @@ internal class SustainSysSamlTestFixture(ITestOutputHelper output) : IAsyncLifet
services.AddSingleton<TimeProvider>(fakeTimeProvider);
services.AddSingleton<IDistributedCache>(sp => new FakeDistributedCache(sp.GetRequiredService<TimeProvider>()));
// Register the mutable service provider list before AddSamlServices
// so TryAdd in AddSamlServices won't overwrite our registration
services.AddSingleton<ISamlServiceProviderStore>(new InMemorySamlServiceProviderStore(_serviceProviders));
services.AddIdentityServer(options =>
{
options.UserInteraction.LoginUrl = "/account/login";
@ -107,7 +102,8 @@ internal class SustainSysSamlTestFixture(ITestOutputHelper output) : IAsyncLifet
options.KeyManagement.Enabled = false;
})
.AddSigningCredential(selfSignedCertificate)
.AddSamlServices();
.AddSaml()
.AddInMemorySamlServiceProviders(_serviceProviders);
},
app =>
{