mirror of
https://github.com/DuendeSoftware/products
synced 2026-05-24 01:18:22 +00:00
Fixed subtle issues introduced in initial code import
This commit is contained in:
parent
900af2eeab
commit
f5c0a3ca70
17 changed files with 288 additions and 166 deletions
|
|
@ -282,8 +282,8 @@ public static class IdentityServerConstants
|
|||
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";
|
||||
public const string SamlLogout = SamlPathPrefix + "/logout";
|
||||
public const string SamlLogoutCallback = SamlPathPrefix + "/logout_callback";
|
||||
|
||||
public static readonly string[] CorsPaths =
|
||||
{
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ internal class SamlErrorResponseXmlSerializer : ISamlResultSerializer<SamlErrorR
|
|||
new XAttribute("Value", result.StatusCode.ToString()));
|
||||
|
||||
// Add sub-status code if provided
|
||||
if (result.SubStatusCode.HasValue)
|
||||
if (result.SubStatusCode?.Value != null)
|
||||
{
|
||||
statusCodeElement.Add(
|
||||
new XElement(protocolNs + "StatusCode",
|
||||
|
|
|
|||
|
|
@ -64,6 +64,12 @@ internal abstract class SamlRequestExtractor<TRequest, TResult>
|
|||
var signature = request.Query[SamlConstants.RequestProperties.Signature].ToString();
|
||||
var sigAlg = request.Query[SamlConstants.RequestProperties.SigAlg].ToString();
|
||||
|
||||
// Normalize empty relay state to null (important for signature validation)
|
||||
if (string.IsNullOrEmpty(relayState))
|
||||
{
|
||||
relayState = null;
|
||||
}
|
||||
|
||||
// HTTP-Redirect uses deflate compression
|
||||
byte[] compressedXmlBytes;
|
||||
try
|
||||
|
|
@ -108,6 +114,12 @@ internal abstract class SamlRequestExtractor<TRequest, TResult>
|
|||
|
||||
var relayState = form[SamlConstants.RequestProperties.RelayState].ToString();
|
||||
|
||||
// Normalize empty relay state to null
|
||||
if (string.IsNullOrEmpty(relayState))
|
||||
{
|
||||
relayState = null;
|
||||
}
|
||||
|
||||
// HTTP-POST has no compression
|
||||
byte[] xmlBytes;
|
||||
try
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ internal class SamlResponseBuilder(
|
|||
ServiceProvider = serviceProvider,
|
||||
Binding = serviceProvider.AssertionConsumerServiceBinding,
|
||||
StatusCode = error.StatusCode,
|
||||
SubStatusCode = error.SubStatusCode,
|
||||
SubStatusCode = error.SubStatusCode != null ? new SamlStatusCode(error.SubStatusCode) : null,
|
||||
Message = error.Message,
|
||||
AssertionConsumerServiceUrl = acsUrl,
|
||||
Issuer = serverUrls.Origin, // Todo: not sure if this is a valid issuer
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ internal class SamlLogoutNotificationService(
|
|||
|
||||
var logoutUrls = new List<ISamlFrontChannelLogout>();
|
||||
|
||||
if (context.SamlSessions?.Any() == true)
|
||||
if (!context.SamlSessions.Any())
|
||||
{
|
||||
logger.NoSamlServiceProvidersToNotifyForLogout(LogLevel.Debug);
|
||||
return logoutUrls;
|
||||
|
|
|
|||
|
|
@ -22,6 +22,11 @@ internal class SamlSingleLogoutCallbackEndpoint(
|
|||
{
|
||||
using var activity = Tracing.BasicActivitySource.StartActivity("SamlSingleLogoutCallbackEndpoint");
|
||||
|
||||
if (!HttpMethods.IsGet(context.Request.Method))
|
||||
{
|
||||
return new StatusCodeResult(HttpStatusCode.MethodNotAllowed);
|
||||
}
|
||||
|
||||
logger.ProcessingSamlLogoutCallbackRequest(LogLevel.Debug);
|
||||
|
||||
var logoutId = context.Request.Query["logoutId"].ToString();
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ internal class SamlSigninStateIdCookie(IHttpContextAccessor httpContextAccessor)
|
|||
Expires = DateTimeOffset.UtcNow.Add(CookieLifetime)
|
||||
};
|
||||
|
||||
HttpContext.Response.Cookies.Append(CookieName, stateId.Value.ToString("N"), cookieOptions);
|
||||
HttpContext.Response.Cookies.Append(CookieName, stateId.Value.ToString(), cookieOptions);
|
||||
}
|
||||
|
||||
internal bool TryGetSamlSigninStateId([NotNullWhen(true)] out StateId? stateId)
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ public class InMemorySamlServiceProviderStore : ISamlServiceProviderStore
|
|||
|
||||
var query =
|
||||
from sp in _serviceProviders
|
||||
where sp.EntityId == entityId
|
||||
where sp.EntityId == entityId && sp.Enabled
|
||||
select sp;
|
||||
|
||||
return Task.FromResult(query.SingleOrDefault());
|
||||
|
|
|
|||
|
|
@ -0,0 +1,62 @@
|
|||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
#nullable enable
|
||||
using Microsoft.Extensions.Caching.Distributed;
|
||||
|
||||
namespace Duende.IdentityServer.IntegrationTests.Common;
|
||||
|
||||
internal class FakeDistributedCache(TimeProvider timeProvider) : IDistributedCache
|
||||
{
|
||||
private readonly Dictionary<string, CacheEntry> _items = new();
|
||||
|
||||
private record CacheEntry(byte[] Value, DateTimeOffset? AbsoluteExpiration);
|
||||
|
||||
public byte[]? Get(string key)
|
||||
{
|
||||
if (!_items.TryGetValue(key, out var entry))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!entry.AbsoluteExpiration.HasValue || timeProvider.GetUtcNow() <= entry.AbsoluteExpiration.Value)
|
||||
{
|
||||
return entry.Value;
|
||||
}
|
||||
|
||||
_items.Remove(key);
|
||||
return null;
|
||||
}
|
||||
|
||||
public Task<byte[]?> GetAsync(string key, CancellationToken token = default) => Task.FromResult(Get(key));
|
||||
|
||||
public void Set(string key, byte[] value, DistributedCacheEntryOptions options)
|
||||
{
|
||||
var absoluteExpiration = options.AbsoluteExpirationRelativeToNow.HasValue
|
||||
? timeProvider.GetUtcNow().Add(options.AbsoluteExpirationRelativeToNow.Value)
|
||||
: options.AbsoluteExpiration;
|
||||
|
||||
_items[key] = new CacheEntry(value, absoluteExpiration);
|
||||
}
|
||||
|
||||
public Task SetAsync(string key, byte[] value, DistributedCacheEntryOptions options, CancellationToken token = default)
|
||||
{
|
||||
Set(key, value, options);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public void Remove(string key) => _items.Remove(key);
|
||||
|
||||
public Task RemoveAsync(string key, CancellationToken token = default)
|
||||
{
|
||||
Remove(key);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public void Refresh(string key)
|
||||
{
|
||||
// not currently needed
|
||||
}
|
||||
|
||||
public Task RefreshAsync(string key, CancellationToken token = default) => Task.CompletedTask;
|
||||
}
|
||||
|
|
@ -6,12 +6,15 @@
|
|||
using System.Security.Claims;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using Duende.IdentityServer.Configuration;
|
||||
using Duende.IdentityServer.IntegrationTests.Common;
|
||||
using Duende.IdentityServer.Models;
|
||||
using Duende.IdentityServer.Saml;
|
||||
using Duende.IdentityServer.Services;
|
||||
using Duende.IdentityServer.Stores;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Caching.Distributed;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Duende.IdentityServer.IntegrationTests.Endpoints.Saml;
|
||||
|
|
@ -69,40 +72,72 @@ internal class SamlFixture : IAsyncLifetime
|
|||
|
||||
public AuthenticationProperties? PropsToSignIn { get; set; }
|
||||
|
||||
public TestFramework.GenericHost? Host { get; private set; }
|
||||
private IdentityServerPipeline _pipeline = null!;
|
||||
|
||||
public HttpClient Client { get; private set; } = null!;
|
||||
public IdentityServerPipeline Pipeline => _pipeline;
|
||||
|
||||
public HttpClient NonRedirectingClient { get; private set; } = null!;
|
||||
public BrowserClient Client { get; private set; } = null!;
|
||||
|
||||
public T Get<T>() where T : notnull => Host!.Resolve<T>();
|
||||
public BrowserClient NonRedirectingClient { get; private set; } = null!;
|
||||
|
||||
public string Url(string path = "")
|
||||
{
|
||||
if (!path.StartsWith('/') && !string.IsNullOrEmpty(path))
|
||||
{
|
||||
path = '/' + path;
|
||||
}
|
||||
|
||||
return IdentityServerPipeline.BaseUrl + path;
|
||||
}
|
||||
|
||||
public T Get<T>() where T : notnull => _pipeline.Resolve<T>();
|
||||
|
||||
public async ValueTask InitializeAsync()
|
||||
{
|
||||
var selfSignedCertificate = X509CertificateLoader.LoadPkcs12(Convert.FromBase64String(StableSigningCert), null);
|
||||
|
||||
Host = new TestFramework.GenericHost();
|
||||
Host.OnConfigureServices += services =>
|
||||
_pipeline = new IdentityServerPipeline();
|
||||
|
||||
_pipeline.OnPreConfigureServices += services =>
|
||||
{
|
||||
services.AddSingleton<TimeProvider>(Data.FakeTimeProvider);
|
||||
services.AddDistributedMemoryCache();
|
||||
services.AddSingleton<IDistributedCache>(sp => new FakeDistributedCache(sp.GetRequiredService<TimeProvider>()));
|
||||
services.AddRouting();
|
||||
services.AddAuthorization();
|
||||
};
|
||||
|
||||
services.AddIdentityServer(options =>
|
||||
_pipeline.OnPostConfigureServices += services =>
|
||||
{
|
||||
// Configure IdentityServer options (pipeline already calls AddIdentityServer)
|
||||
services.Configure<IdentityServerOptions>(options =>
|
||||
{
|
||||
options.UserInteraction.LoginUrl = LoginUrl.ToString();
|
||||
options.UserInteraction.LogoutUrl = LogoutUrl.ToString();
|
||||
options.UserInteraction.ConsentUrl = ConsentUrl.ToString();
|
||||
ConfigureIdentityServerOptions(options);
|
||||
})
|
||||
.AddSigningCredential(selfSignedCertificate)
|
||||
.AddSamlServices();
|
||||
options.KeyManagement.Enabled = false; // Disable key management to use our custom credential
|
||||
});
|
||||
services.Configure(ConfigureIdentityServerOptions);
|
||||
|
||||
// Configure SAML options
|
||||
services.Configure<SamlOptions>(ConfigureSamlOptions);
|
||||
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));
|
||||
if (signingCredentialDescriptor != null)
|
||||
{
|
||||
services.Remove(signingCredentialDescriptor);
|
||||
}
|
||||
|
||||
// Add our X509 signing credential
|
||||
services.AddIdentityServerBuilder()
|
||||
.AddSigningCredential(selfSignedCertificate)
|
||||
.AddProfileService<DefaultProfileService>()
|
||||
.AddSamlServices();
|
||||
|
||||
ConfigureServices(services);
|
||||
|
||||
services.AddProblemDetails(opt => opt.CustomizeProblemDetails = context =>
|
||||
|
|
@ -116,123 +151,137 @@ internal class SamlFixture : IAsyncLifetime
|
|||
});
|
||||
};
|
||||
|
||||
Host.OnConfigure += app =>
|
||||
_pipeline.OnPreConfigure += app =>
|
||||
{
|
||||
app.UseExceptionHandler();
|
||||
|
||||
app.UseIdentityServer();
|
||||
|
||||
app.MapGet(LoginUrl.ToString(), () => Microsoft.AspNetCore.Http.Results.Ok());
|
||||
app.MapGet(ConsentUrl.ToString(), () => Microsoft.AspNetCore.Http.Results.Ok());
|
||||
app.MapGet(LogoutUrl.ToString(), () => Microsoft.AspNetCore.Http.Results.Ok());
|
||||
|
||||
app.MapGet("/__signin", async (HttpContext ctx, ISamlInteractionService samlInteractionService) =>
|
||||
{
|
||||
var props = PropsToSignIn ?? new AuthenticationProperties();
|
||||
if (UserToSignIn?.Identity == null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Must set {nameof(UserToSignIn)} prior to signin and must have an identity");
|
||||
}
|
||||
|
||||
await ctx.SignInAsync(UserToSignIn, props);
|
||||
|
||||
if (UserMetRequestedAuthnContextRequirements.HasValue)
|
||||
{
|
||||
await samlInteractionService.StoreRequestedAuthnContextResultAsync(
|
||||
UserMetRequestedAuthnContextRequirements.Value, ctx.RequestAborted);
|
||||
}
|
||||
|
||||
ctx.Response.StatusCode = 204;
|
||||
});
|
||||
|
||||
app.MapGet("/__signout", async ctx =>
|
||||
{
|
||||
await ctx.SignOutAsync();
|
||||
ctx.Response.StatusCode = 204;
|
||||
});
|
||||
|
||||
app.MapGet("/__authentication-request", async (ISamlInteractionService samlInteractionService) =>
|
||||
{
|
||||
var authenticationRequest =
|
||||
await samlInteractionService.GetAuthenticationRequestContextAsync(CancellationToken.None);
|
||||
|
||||
if (authenticationRequest == null)
|
||||
{
|
||||
throw new InvalidOperationException("Could not find authentication request");
|
||||
}
|
||||
|
||||
return authenticationRequest.RequestedAuthnContext;
|
||||
});
|
||||
|
||||
app.MapGet("/__protected-resource", () => "Protected Resource").RequireAuthorization();
|
||||
app.UseExceptionHandler("/error");
|
||||
};
|
||||
|
||||
await Host.InitializeAsync();
|
||||
_pipeline.OnPostConfigure += app =>
|
||||
{
|
||||
// Error handling endpoint
|
||||
app.Map("/error", path =>
|
||||
{
|
||||
path.Run(async context =>
|
||||
{
|
||||
var exceptionFeature = context.Features.Get<Microsoft.AspNetCore.Diagnostics.IExceptionHandlerFeature>();
|
||||
if (exceptionFeature?.Error is Microsoft.AspNetCore.Http.BadHttpRequestException badRequestEx)
|
||||
{
|
||||
context.Response.StatusCode = badRequestEx.StatusCode;
|
||||
context.Response.ContentType = "application/problem+json";
|
||||
await context.Response.WriteAsJsonAsync(new Microsoft.AspNetCore.Mvc.ProblemDetails
|
||||
{
|
||||
Status = badRequestEx.StatusCode,
|
||||
Title = "Bad Request",
|
||||
Detail = badRequestEx.Message
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
context.Response.StatusCode = 500;
|
||||
await context.Response.WriteAsync("Internal Server Error");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
app.Map(LoginUrl.ToString(), path =>
|
||||
{
|
||||
path.Run(ctx =>
|
||||
{
|
||||
ctx.Response.StatusCode = 200;
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
});
|
||||
|
||||
app.Map(ConsentUrl.ToString(), path =>
|
||||
{
|
||||
path.Run(ctx =>
|
||||
{
|
||||
ctx.Response.StatusCode = 200;
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
});
|
||||
|
||||
app.Map(LogoutUrl.ToString(), path =>
|
||||
{
|
||||
path.Run(ctx =>
|
||||
{
|
||||
ctx.Response.StatusCode = 200;
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
});
|
||||
|
||||
app.Map("/__signin", path =>
|
||||
{
|
||||
path.Run(async ctx =>
|
||||
{
|
||||
var samlInteractionService = ctx.RequestServices.GetRequiredService<ISamlInteractionService>();
|
||||
var props = PropsToSignIn ?? new AuthenticationProperties();
|
||||
if (UserToSignIn?.Identity == null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Must set {nameof(UserToSignIn)} prior to signin and must have an identity");
|
||||
}
|
||||
|
||||
await ctx.SignInAsync(UserToSignIn, props);
|
||||
|
||||
if (UserMetRequestedAuthnContextRequirements.HasValue)
|
||||
{
|
||||
await samlInteractionService.StoreRequestedAuthnContextResultAsync(
|
||||
UserMetRequestedAuthnContextRequirements.Value, ctx.RequestAborted);
|
||||
}
|
||||
|
||||
ctx.Response.StatusCode = 204;
|
||||
});
|
||||
});
|
||||
|
||||
app.Map("/__signout", path =>
|
||||
{
|
||||
path.Run(async ctx =>
|
||||
{
|
||||
await ctx.SignOutAsync();
|
||||
ctx.Response.StatusCode = 204;
|
||||
});
|
||||
});
|
||||
|
||||
app.Map("/__authentication-request", path =>
|
||||
{
|
||||
path.Run(async ctx =>
|
||||
{
|
||||
var samlInteractionService = ctx.RequestServices.GetRequiredService<ISamlInteractionService>();
|
||||
var authenticationRequest =
|
||||
await samlInteractionService.GetAuthenticationRequestContextAsync(CancellationToken.None);
|
||||
|
||||
if (authenticationRequest == null)
|
||||
{
|
||||
throw new InvalidOperationException("Could not find authentication request");
|
||||
}
|
||||
|
||||
await ctx.Response.WriteAsJsonAsync(authenticationRequest.RequestedAuthnContext);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
_pipeline.Initialize(enableLogging: true);
|
||||
|
||||
// Mark as initialized after seeding
|
||||
_isInitialized = true;
|
||||
|
||||
Client = Host!.HttpClient;
|
||||
NonRedirectingClient = Host!.Server.CreateClient();
|
||||
// Create two BrowserClient instances with different redirect behaviors
|
||||
Client = _pipeline.BrowserClient;
|
||||
Client.BaseAddress = new Uri(IdentityServerPipeline.BaseUrl);
|
||||
|
||||
NonRedirectingClient = new BrowserClient(new BrowserHandler(_pipeline.Handler) { AllowAutoRedirect = false })
|
||||
{
|
||||
BaseAddress = new Uri(IdentityServerPipeline.BaseUrl)
|
||||
};
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (Host != null)
|
||||
{
|
||||
// GenericHost doesn't implement IAsyncDisposable, so nothing to dispose
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a service provider to the fixture after initialization.
|
||||
/// </summary>
|
||||
public async Task AddServiceProviderAsync(SamlServiceProvider serviceProvider)
|
||||
{
|
||||
if (!_isInitialized)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Cannot call AddServiceProviderAsync before initialization. " +
|
||||
"Add service providers to the ServiceProviders list before calling InitializeAsync.");
|
||||
}
|
||||
|
||||
if (Host == null)
|
||||
{
|
||||
throw new InvalidOperationException("Host is not initialized");
|
||||
}
|
||||
|
||||
// With InMemorySamlServiceProviderStore, we need to replace the store instance
|
||||
// This is a limitation - in integration tests, we should add all SPs before initialization
|
||||
_serviceProviders.Add(serviceProvider);
|
||||
|
||||
// Re-register the store with the updated list
|
||||
var serviceProvider2 = Host.Server.Services;
|
||||
var scope = serviceProvider2.CreateScope();
|
||||
|
||||
// This won't work properly with the current architecture
|
||||
// The store is registered as a singleton, so we can't easily update it
|
||||
// For now, throw an exception to indicate this is not supported
|
||||
throw new NotSupportedException(
|
||||
"Adding service providers after initialization is not currently supported with InMemorySamlServiceProviderStore. " +
|
||||
"Please add all service providers to the ServiceProviders list before calling InitializeAsync.");
|
||||
}
|
||||
public async ValueTask DisposeAsync() =>
|
||||
// IdentityServerPipeline doesn't implement IAsyncDisposable, so nothing to dispose
|
||||
await Task.CompletedTask;
|
||||
|
||||
/// <summary>
|
||||
/// Removes all service providers from the fixture after initialization.
|
||||
/// </summary>
|
||||
public async Task ClearServiceProvidersAsync()
|
||||
{
|
||||
if (!_isInitialized)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Cannot call ClearServiceProvidersAsync before initialization. " +
|
||||
"Modify the ServiceProviders list directly before calling InitializeAsync.");
|
||||
}
|
||||
|
||||
throw new NotSupportedException(
|
||||
"Clearing service providers after initialization is not currently supported with InMemorySamlServiceProviderStore. " +
|
||||
"The service provider store is immutable after initialization.");
|
||||
}
|
||||
public void ClearServiceProvidersAsync() => _serviceProviders.Clear();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -292,7 +292,7 @@ public class SamlIdpInitiatedEndpointTests
|
|||
var samlResponse = await ExtractSamlSuccessFromPostAsync(callbackResult, CancellationToken.None);
|
||||
|
||||
samlResponse.ShouldNotBeNull();
|
||||
samlResponse.Issuer.ShouldBe(Fixture.Host!.Url());
|
||||
samlResponse.Issuer.ShouldBe(Fixture.Url());
|
||||
samlResponse.Destination.ShouldBe(Data.AcsUrl.ToString());
|
||||
samlResponse.StatusCode.ShouldBe("urn:oasis:names:tc:SAML:2.0:status:Success");
|
||||
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ public class SamlMetadataEndpointTests
|
|||
var content = await result.Content.ReadAsStringAsync(CancellationToken.None);
|
||||
|
||||
var settings = new VerifySettings();
|
||||
var hostUri = Fixture.Host!.Url();
|
||||
var hostUri = Fixture.Url();
|
||||
settings.AddScrubber(sb =>
|
||||
{
|
||||
sb.Replace(hostUri, "https://localhost");
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
<?xml version="1.0" encoding="utf-8"?><EntityDescriptor entityID="http://localhost" validUntil="2000-01-09T03:04:05Z" xmlns="urn:oasis:names:tc:SAML:2.0:metadata"><IDPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol"><KeyDescriptor use="signing"><KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"><X509Data><X509Certificate>MIICojCCAYqgAwIBAgIIIjGqKDo3ME4wDQYJKoZIhvcNAQELBQAwETEPMA0GA1UEAxMGZm9vYmFyMB4XDTI1MTExMDE0MTEyNloXDTMwMTExMDE0MTEyNlowETEPMA0GA1UEAxMGZm9vYmFyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu/fcM55jlB810lyxGpgk0Zhw83Liqz80l3zLLAZgJ/IUdBx9VFD28BeO37eByHDXxBIQdHFYXQj+lv2g3KFRxVzfZhiFUrb1UydJYFZ951sQUEsP4T/Fpbyb95HNrwG2NwE5/fk1MXr9no4ydsQTZA6EWOfbxn6o2YQs/8QdDykhCzpZcWYbk5AKS/G6nYLpwuW4UsyMQ6ur9ZQXtwDS/hGyP3RjK8pjqkckbQG9ZapI+hWezIJkGmkXcuIx+FpZbdjjwu/SIcNNrBIXLbrbWyxoWt4y2jWfDixanBAubBLtx6tCg69trJ3M5gZkFZBR3CVqs78fYZUThKBTS20afQIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQCxB08tE2bDWpF5mR14kQvRUA/2hZKeC6CYYGEwOu1hbh5m3rVj4T9GPgOh+s6tX+rCb0IoV1uD9iSeTd3XaJ/1sSFkgVD/PaA6NRgzKVeDXLl9rZGAnOmp/Es3Pz35FbPxZKTe8UDyFHySbioLaLvtODhzX7SeGP3BcRpp8rZLvggMYiqo3w39+qZcgZPIBP4yRSulBYb3r9qagQ/n//gp7SmenCQmjA5L7pTn7QggFQsSQmB6dyNS54cUk0niUsTihT9oqpMnXmsXonXf5cv3tnaydreiB4aPea+OjjY3oy8hvHUH6FuQQX7t3RllZlPGJQFZe61rYMVmRRjlHWTA</X509Certificate></X509Data></KeyInfo></KeyDescriptor><NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</NameIDFormat><NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:persistent</NameIDFormat><NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</NameIDFormat><NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</NameIDFormat><SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="http://localhost/saml/signin" /><SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="http://localhost/saml/signin" /><SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="http://localhost/saml/logout" /><SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="http://localhost/saml/logout" /></IDPSSODescriptor></EntityDescriptor>
|
||||
<?xml version="1.0" encoding="utf-8"?><EntityDescriptor entityID="https://localhost" validUntil="2000-01-09T03:04:05Z" xmlns="urn:oasis:names:tc:SAML:2.0:metadata"><IDPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol"><KeyDescriptor use="signing"><KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"><X509Data><X509Certificate>MIICojCCAYqgAwIBAgIIIjGqKDo3ME4wDQYJKoZIhvcNAQELBQAwETEPMA0GA1UEAxMGZm9vYmFyMB4XDTI1MTExMDE0MTEyNloXDTMwMTExMDE0MTEyNlowETEPMA0GA1UEAxMGZm9vYmFyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu/fcM55jlB810lyxGpgk0Zhw83Liqz80l3zLLAZgJ/IUdBx9VFD28BeO37eByHDXxBIQdHFYXQj+lv2g3KFRxVzfZhiFUrb1UydJYFZ951sQUEsP4T/Fpbyb95HNrwG2NwE5/fk1MXr9no4ydsQTZA6EWOfbxn6o2YQs/8QdDykhCzpZcWYbk5AKS/G6nYLpwuW4UsyMQ6ur9ZQXtwDS/hGyP3RjK8pjqkckbQG9ZapI+hWezIJkGmkXcuIx+FpZbdjjwu/SIcNNrBIXLbrbWyxoWt4y2jWfDixanBAubBLtx6tCg69trJ3M5gZkFZBR3CVqs78fYZUThKBTS20afQIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQCxB08tE2bDWpF5mR14kQvRUA/2hZKeC6CYYGEwOu1hbh5m3rVj4T9GPgOh+s6tX+rCb0IoV1uD9iSeTd3XaJ/1sSFkgVD/PaA6NRgzKVeDXLl9rZGAnOmp/Es3Pz35FbPxZKTe8UDyFHySbioLaLvtODhzX7SeGP3BcRpp8rZLvggMYiqo3w39+qZcgZPIBP4yRSulBYb3r9qagQ/n//gp7SmenCQmjA5L7pTn7QggFQsSQmB6dyNS54cUk0niUsTihT9oqpMnXmsXonXf5cv3tnaydreiB4aPea+OjjY3oy8hvHUH6FuQQX7t3RllZlPGJQFZe61rYMVmRRjlHWTA</X509Certificate></X509Data></KeyInfo></KeyDescriptor><NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</NameIDFormat><NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:persistent</NameIDFormat><NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</NameIDFormat><NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</NameIDFormat><SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://localhost/saml/signin" /><SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://localhost/saml/signin" /><SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://localhost/saml/logout" /><SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://localhost/saml/logout" /></IDPSSODescriptor></EntityDescriptor>
|
||||
|
|
|
|||
|
|
@ -129,7 +129,7 @@ public class SamlSigninCallbackEndpointTests
|
|||
var redirectUri = signinResult.Headers.Location;
|
||||
redirectUri.ShouldNotBeNull();
|
||||
|
||||
await Fixture.ClearServiceProvidersAsync();
|
||||
Fixture.ClearServiceProvidersAsync();
|
||||
|
||||
var result = await Fixture.NonRedirectingClient.GetAsync("/saml/signin_callback", CancellationToken.None);
|
||||
|
||||
|
|
@ -159,10 +159,9 @@ public class SamlSigninCallbackEndpointTests
|
|||
var redirectUri = signinResult.Headers.Location;
|
||||
redirectUri.ShouldNotBeNull();
|
||||
|
||||
await Fixture.ClearServiceProvidersAsync();
|
||||
var disabledSp = Build.SamlServiceProvider();
|
||||
// Ideally we would fetch the SP via a store and update it, but since the store doesn't provide that functionality
|
||||
// we'll rely on everything being in memory and holding onto a reference to the SP in the store for now
|
||||
sp.Enabled = false;
|
||||
await Fixture.AddServiceProviderAsync(disabledSp);
|
||||
|
||||
var result = await Fixture.NonRedirectingClient.GetAsync("/saml/signin_callback", CancellationToken.None);
|
||||
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ public class SamlSigninEndpointTests
|
|||
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!.Url());
|
||||
errorResponse.Issuer.ShouldBe(Fixture.Url());
|
||||
errorResponse.InResponseTo.ShouldNotBeNullOrEmpty();
|
||||
errorResponse.AssertionConsumerServiceUrl.ShouldBe(Data.AcsUrl.ToString());
|
||||
}
|
||||
|
|
@ -222,7 +222,7 @@ public class SamlSigninEndpointTests
|
|||
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!.Url());
|
||||
errorResponse.Issuer.ShouldBe(Fixture.Url());
|
||||
errorResponse.InResponseTo.ShouldNotBeNullOrEmpty();
|
||||
errorResponse.AssertionConsumerServiceUrl.ShouldBe(Data.AcsUrl.ToString());
|
||||
}
|
||||
|
|
@ -315,7 +315,7 @@ public class SamlSigninEndpointTests
|
|||
|
||||
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!.Url());
|
||||
errorResponse.Issuer.ShouldBe(Fixture.Url());
|
||||
errorResponse.InResponseTo.ShouldNotBeNullOrEmpty();
|
||||
errorResponse.AssertionConsumerServiceUrl.ShouldBe(Data.AcsUrl.ToString());
|
||||
}
|
||||
|
|
@ -497,7 +497,7 @@ public class SamlSigninEndpointTests
|
|||
await Fixture.InitializeAsync();
|
||||
|
||||
// Use the correct Destination attribute
|
||||
var correctDestination = new Uri(Fixture.Host!.Url() + "/saml/signin");
|
||||
var correctDestination = new Uri(Fixture.Url() + "/saml/signin");
|
||||
var authnRequestXml = Build.AuthNRequestXml(destination: correctDestination);
|
||||
|
||||
var urlEncoded = await EncodeRequest(authnRequestXml);
|
||||
|
|
@ -539,7 +539,7 @@ public class SamlSigninEndpointTests
|
|||
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?.Url());
|
||||
successResponse.Issuer.ShouldBe(Fixture.Url());
|
||||
successResponse.InResponseTo.ShouldBe(Data.RequestId);
|
||||
|
||||
successResponse.StatusCode.ShouldBe(SamlStatusCode.Success.Value);
|
||||
|
|
@ -549,7 +549,7 @@ public class SamlSigninEndpointTests
|
|||
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?.Url());
|
||||
assertion.Issuer.ShouldBe(Fixture.Url());
|
||||
|
||||
var subject = assertion.Subject;
|
||||
subject.ShouldNotBeNull();
|
||||
|
|
@ -788,7 +788,7 @@ public class SamlSigninEndpointTests
|
|||
|
||||
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!.Url());
|
||||
errorResponse.Issuer.ShouldBe(Fixture.Url());
|
||||
errorResponse.InResponseTo.ShouldNotBeNullOrEmpty();
|
||||
errorResponse.AssertionConsumerServiceUrl.ShouldBe(Data.AcsUrl.ToString());
|
||||
}
|
||||
|
|
@ -813,7 +813,7 @@ public class SamlSigninEndpointTests
|
|||
|
||||
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!.Url());
|
||||
errorResponse.Issuer.ShouldBe(Fixture.Url());
|
||||
errorResponse.InResponseTo.ShouldNotBeNullOrEmpty();
|
||||
errorResponse.AssertionConsumerServiceUrl.ShouldBe(Data.AcsUrl.ToString());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -150,7 +150,7 @@ public class SamlSingleLogoutEndpointTests
|
|||
await Fixture.Client.GetAsync("/__signin", CancellationToken.None);
|
||||
|
||||
var logoutRequestXml = Build.LogoutRequestXml(
|
||||
destination: new Uri($"{Fixture.Host!.Url()}/saml/logout"),
|
||||
destination: new Uri($"{Fixture.Url()}/saml/logout"),
|
||||
sessionIndex: "session123");
|
||||
var urlEncoded = await EncodeAndSignRequest(logoutRequestXml, sp, CancellationToken.None);
|
||||
|
||||
|
|
@ -175,7 +175,7 @@ public class SamlSingleLogoutEndpointTests
|
|||
await Fixture.InitializeAsync();
|
||||
|
||||
var logoutRequestXml = Build.LogoutRequestXml(
|
||||
destination: new Uri($"{Fixture.Host!.Url()}/saml/logout"),
|
||||
destination: new Uri($"{Fixture.Url()}/saml/logout"),
|
||||
version: "1.0");
|
||||
var urlEncoded = await EncodeAndSignRequest(logoutRequestXml, sp, CancellationToken.None);
|
||||
|
||||
|
|
@ -199,7 +199,7 @@ public class SamlSingleLogoutEndpointTests
|
|||
|
||||
var futureTime = Data.Now.AddMinutes(10);
|
||||
var logoutRequestXml = Build.LogoutRequestXml(
|
||||
destination: new Uri($"{Fixture.Host!.Url()}/saml/logout"),
|
||||
destination: new Uri($"{Fixture.Url()}/saml/logout"),
|
||||
issueInstant: futureTime,
|
||||
sessionIndex: "session123");
|
||||
var urlEncoded = await EncodeAndSignRequest(logoutRequestXml, sp, CancellationToken.None);
|
||||
|
|
@ -225,7 +225,7 @@ public class SamlSingleLogoutEndpointTests
|
|||
|
||||
var oldTime = Data.Now.AddMinutes(-10);
|
||||
var logoutRequestXml = Build.LogoutRequestXml(
|
||||
destination: new Uri($"{Fixture.Host!.Url()}/saml/logout"),
|
||||
destination: new Uri($"{Fixture.Url()}/saml/logout"),
|
||||
issueInstant: oldTime,
|
||||
sessionIndex: "session123");
|
||||
var urlEncoded = await EncodeAndSignRequest(logoutRequestXml, sp, CancellationToken.None);
|
||||
|
|
@ -260,7 +260,7 @@ public class SamlSingleLogoutEndpointTests
|
|||
// Assert
|
||||
var logoutResponse = await ExtractSamlLogoutResponseFromPostAsync(result, CancellationToken.None);
|
||||
logoutResponse.StatusCode.ShouldBe(SamlStatusCode.Requester.Value);
|
||||
logoutResponse.StatusMessage.ShouldBe($"Invalid destination. Expected '{Fixture.Host!.Url()}/saml/logout'");
|
||||
logoutResponse.StatusMessage.ShouldBe($"Invalid destination. Expected '{Fixture.Url()}/saml/logout'");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -274,7 +274,7 @@ public class SamlSingleLogoutEndpointTests
|
|||
await Fixture.InitializeAsync();
|
||||
|
||||
var logoutRequestXml = Build.LogoutRequestXml(
|
||||
destination: new Uri($"{Fixture.Host!.Url()}/saml/logout"),
|
||||
destination: new Uri($"{Fixture.Url()}/saml/logout"),
|
||||
sessionIndex: "session123");
|
||||
var urlEncoded = await EncodeRequest(logoutRequestXml, CancellationToken.None);
|
||||
|
||||
|
|
@ -300,7 +300,7 @@ public class SamlSingleLogoutEndpointTests
|
|||
|
||||
// Create a logout request without a signature
|
||||
var logoutRequestXml = Build.LogoutRequestXml(
|
||||
destination: new Uri($"{Fixture.Host!.Url()}/saml/logout"),
|
||||
destination: new Uri($"{Fixture.Url()}/saml/logout"),
|
||||
sessionIndex: "session123");
|
||||
var urlEncoded = await EncodeRequest(logoutRequestXml, CancellationToken.None);
|
||||
|
||||
|
|
@ -351,7 +351,7 @@ public class SamlSingleLogoutEndpointTests
|
|||
// Don't sign in a user - no authenticated session
|
||||
|
||||
var logoutRequestXml = Build.LogoutRequestXml(
|
||||
destination: new Uri($"{Fixture.Host!.Url()}/saml/logout"),
|
||||
destination: new Uri($"{Fixture.Url()}/saml/logout"),
|
||||
sessionIndex: "session123");
|
||||
var urlEncoded = await EncodeAndSignRequest(logoutRequestXml, sp, CancellationToken.None);
|
||||
|
||||
|
|
@ -384,7 +384,7 @@ public class SamlSingleLogoutEndpointTests
|
|||
// 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!.Url()}/saml/logout"));
|
||||
destination: new Uri($"{Fixture.Url()}/saml/logout"));
|
||||
var urlEncoded = await EncodeAndSignRequest(logoutRequestXml, sp, CancellationToken.None);
|
||||
|
||||
// Act
|
||||
|
|
@ -412,7 +412,7 @@ public class SamlSingleLogoutEndpointTests
|
|||
|
||||
// Use a different session index than what was established
|
||||
var logoutRequestXml = Build.LogoutRequestXml(
|
||||
destination: new Uri($"{Fixture.Host!.Url()}/saml/logout"),
|
||||
destination: new Uri($"{Fixture.Url()}/saml/logout"),
|
||||
sessionIndex: "wrong-session-index");
|
||||
var urlEncoded = await EncodeAndSignRequest(logoutRequestXml, sp, CancellationToken.None);
|
||||
|
||||
|
|
@ -443,7 +443,7 @@ public class SamlSingleLogoutEndpointTests
|
|||
var sessionIndex = await PerformSigninAndExtractSessionIndex(sp);
|
||||
|
||||
var logoutRequestXml = Build.LogoutRequestXml(
|
||||
destination: new Uri($"{Fixture.Host!.Url()}/saml/logout"),
|
||||
destination: new Uri($"{Fixture.Url()}/saml/logout"),
|
||||
sessionIndex: sessionIndex);
|
||||
var urlEncoded = await EncodeAndSignRequest(logoutRequestXml, sp, CancellationToken.None);
|
||||
|
||||
|
|
@ -481,7 +481,7 @@ public class SamlSingleLogoutEndpointTests
|
|||
var sessionIndex = await PerformSigninAndExtractSessionIndex(sp);
|
||||
|
||||
var logoutRequestXml = Build.LogoutRequestXml(
|
||||
destination: new Uri($"{Fixture.Host!.Url()}/saml/logout"),
|
||||
destination: new Uri($"{Fixture.Url()}/saml/logout"),
|
||||
sessionIndex: sessionIndex);
|
||||
var urlEncoded = await EncodeAndSignRequest(logoutRequestXml, sp, CancellationToken.None);
|
||||
|
||||
|
|
@ -494,7 +494,7 @@ public class SamlSingleLogoutEndpointTests
|
|||
// 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!.Url()}{Fixture.LoginUrl.ToString()}");
|
||||
finalProtectedResourceResult.RequestMessage?.RequestUri?.AbsoluteUri.ShouldStartWith($"{Fixture.Url()}{Fixture.LoginUrl.ToString()}");
|
||||
}
|
||||
|
||||
private static async Task<string> EncodeAndSignRequest(
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ internal class SustainSysSamlTestFixture : IAsyncLifetime
|
|||
public HttpClient? BrowserClient = null!;
|
||||
public X509Certificate2? SigningCertificate { get; private set; }
|
||||
|
||||
public Uri IdentityProviderLoginUri => new Uri(new Uri(_samlFixture.Host!.Url()), _samlFixture.LoginUrl);
|
||||
public Uri IdentityProviderLoginUri => new Uri(new Uri(_samlFixture.Url()), _samlFixture.LoginUrl);
|
||||
|
||||
private readonly SamlFixture _samlFixture = new();
|
||||
private bool _shouldGenerateSigningCertificate;
|
||||
|
|
@ -37,7 +37,7 @@ internal class SustainSysSamlTestFixture : IAsyncLifetime
|
|||
{
|
||||
_samlFixture.UserToSignIn =
|
||||
new ClaimsPrincipal(new ClaimsIdentity([new Claim(JwtClaimTypes.Subject, "user-id"), new Claim("name", "Test User"), new Claim(JwtClaimTypes.AuthenticationMethod, "urn:oasis:names:tc:SAML:2.0:ac:classes:Password")], "Test"));
|
||||
await BrowserClient!.GetAsync($"{_samlFixture.Host!.Url()}/__signin", CancellationToken.None);
|
||||
await BrowserClient!.GetAsync($"{_samlFixture.Url()}/__signin", CancellationToken.None);
|
||||
}
|
||||
|
||||
public void GenerateSigningCertificate() =>
|
||||
|
|
@ -64,11 +64,8 @@ internal class SustainSysSamlTestFixture : IAsyncLifetime
|
|||
publicCertificate = X509CertificateLoader.LoadCertificate(signingCertificate.Export(X509ContentType.Cert));
|
||||
}
|
||||
|
||||
// Initialize the SAML fixture first so we can get the IDP URI
|
||||
await _samlFixture.InitializeAsync();
|
||||
|
||||
// Now initialize the service provider host with the correct IDP URI
|
||||
await InitializeServiceProvider(_samlFixture.Host!.Url(), signingCertificate);
|
||||
// Initialize SP host first so Host is set when creating SP config for IdP
|
||||
await InitializeServiceProvider(_samlFixture.Url(), signingCertificate);
|
||||
|
||||
// Configure the service provider with the actual host URI and add it to the SAML fixture
|
||||
var serviceProvider = new SamlServiceProvider
|
||||
|
|
@ -86,12 +83,10 @@ internal class SustainSysSamlTestFixture : IAsyncLifetime
|
|||
|
||||
// Note: With InMemorySamlServiceProviderStore, we cannot add SPs after initialization
|
||||
// So we need to add it to the fixture before initialization
|
||||
// This is a known limitation of the current implementation
|
||||
// For now, we'll add it to the _samlFixture.ServiceProviders list before it was initialized
|
||||
// But since we already initialized it, we need to work around this
|
||||
// The best approach is to initialize both fixtures together, but that requires refactoring
|
||||
// For now, we'll just note this limitation
|
||||
_samlFixture.ServiceProviders.Add(serviceProvider);
|
||||
|
||||
// Initialize the SAML fixture first so we can get the IDP URI
|
||||
await _samlFixture.InitializeAsync();
|
||||
}
|
||||
|
||||
private async Task InitializeServiceProvider(string identityProviderHostUri, X509Certificate2? signingCertificate = null)
|
||||
|
|
|
|||
Loading…
Reference in a new issue