mirror of
https://github.com/DuendeSoftware/products
synced 2026-05-24 09:28:24 +00:00
Updated SAML code to participate in cooperative cancellation
This commit is contained in:
parent
65afc5d0b1
commit
6a0a2f4a32
60 changed files with 520 additions and 458 deletions
|
|
@ -71,7 +71,7 @@ public static class HttpContextExtensions
|
|||
}
|
||||
|
||||
var samlEntityIds = samlSessions.Select(s => s.EntityId);
|
||||
if (await AnyClientHasFrontChannelLogout(logoutMessage.ClientIds) || await AnySamlServiceProviderHasFrontChannelLogout(samlEntityIds))
|
||||
if (await AnyClientHasFrontChannelLogout(logoutMessage.ClientIds) || await AnySamlServiceProviderHasFrontChannelLogout(samlEntityIds, context.RequestAborted))
|
||||
{
|
||||
endSessionMsg = new LogoutNotificationContext
|
||||
{
|
||||
|
|
@ -90,7 +90,7 @@ public static class HttpContextExtensions
|
|||
var samlEntityIds = samlSessions.Select(s => s.EntityId);
|
||||
|
||||
if ((clientIds.Any() && await AnyClientHasFrontChannelLogout(clientIds)) ||
|
||||
(samlEntityIds.Any() && await AnySamlServiceProviderHasFrontChannelLogout(samlEntityIds)))
|
||||
(samlEntityIds.Any() && await AnySamlServiceProviderHasFrontChannelLogout(samlEntityIds, context.RequestAborted)))
|
||||
{
|
||||
endSessionMsg = new LogoutNotificationContext
|
||||
{
|
||||
|
|
@ -135,12 +135,12 @@ public static class HttpContextExtensions
|
|||
return false;
|
||||
}
|
||||
|
||||
async Task<bool> AnySamlServiceProviderHasFrontChannelLogout(IEnumerable<string> entityIds)
|
||||
async Task<bool> AnySamlServiceProviderHasFrontChannelLogout(IEnumerable<string> entityIds, Ct ct)
|
||||
{
|
||||
var serviceProviderStore = context.RequestServices.GetRequiredService<ISamlServiceProviderStore>();
|
||||
foreach (var entityId in entityIds)
|
||||
{
|
||||
var sp = await serviceProviderStore.FindByEntityIdAsync(entityId);
|
||||
var sp = await serviceProviderStore.FindByEntityIdAsync(entityId, ct);
|
||||
if (sp?.Enabled == true && sp.SingleLogoutServiceUrl != null)
|
||||
{
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ internal class DefaultSamlInteractionService(
|
|||
ILogger<DefaultSamlInteractionService> logger)
|
||||
: ISamlInteractionService
|
||||
{
|
||||
public async Task<SamlAuthenticationRequest?> GetAuthenticationRequestContextAsync(CT ct = default)
|
||||
public async Task<SamlAuthenticationRequest?> GetAuthenticationRequestContextAsync(Ct ct = default)
|
||||
{
|
||||
using var activity = Tracing.ServiceActivitySource.StartActivity("DefaultSamlInteractionService.GetAuthenticationRequestContext");
|
||||
|
||||
|
|
@ -34,7 +34,7 @@ internal class DefaultSamlInteractionService(
|
|||
return null;
|
||||
}
|
||||
|
||||
var sp = await serviceProviderStore.FindByEntityIdAsync(state.ServiceProviderEntityId);
|
||||
var sp = await serviceProviderStore.FindByEntityIdAsync(state.ServiceProviderEntityId, ct);
|
||||
if (sp == null)
|
||||
{
|
||||
logger.ServiceProviderNotFound(LogLevel.Warning, state.ServiceProviderEntityId);
|
||||
|
|
@ -52,7 +52,7 @@ internal class DefaultSamlInteractionService(
|
|||
};
|
||||
}
|
||||
|
||||
public async Task StoreRequestedAuthnContextResultAsync(bool requestedAuthnContextRequirementsWereMet, CT ct = default)
|
||||
public async Task StoreRequestedAuthnContextResultAsync(bool requestedAuthnContextRequirementsWereMet, Ct ct = default)
|
||||
{
|
||||
using var activity = Tracing.ServiceActivitySource.StartActivity("DefaultSamlInteractionService.StoreRequestedAuthnContextResult");
|
||||
|
||||
|
|
|
|||
|
|
@ -8,5 +8,5 @@ namespace Duende.IdentityServer.Internal.Saml;
|
|||
|
||||
internal class EmptySamlServiceProviderStore : ISamlServiceProviderStore
|
||||
{
|
||||
public Task<SamlServiceProvider> FindByEntityIdAsync(string entityId) => Task.FromResult<SamlServiceProvider>(null);
|
||||
public Task<SamlServiceProvider> FindByEntityIdAsync(string entityId, Ct ct) => Task.FromResult<SamlServiceProvider>(null);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,19 +13,21 @@ internal interface ISamlSigningService
|
|||
/// <summary>
|
||||
/// Gets the X509 certificate used for signing SAML messages.
|
||||
/// </summary>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <returns>The signing certificate with private key.</returns>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// Thrown when no signing credential is available, when the credential is not an X509 certificate,
|
||||
/// or when the certificate does not have a private key.
|
||||
/// </exception>
|
||||
Task<X509Certificate2> GetSigningCertificateAsync();
|
||||
Task<X509Certificate2> GetSigningCertificateAsync(Ct ct);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the X509 certificate as a base64-encoded string for inclusion in SAML metadata.
|
||||
/// </summary>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <returns>Base64-encoded certificate bytes.</returns>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// Thrown when no signing credential is available or when the credential is not an X509 certificate.
|
||||
/// </exception>
|
||||
Task<string> GetSigningCertificateBase64Async();
|
||||
Task<string> GetSigningCertificateBase64Async(Ct ct);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,12 +14,12 @@ internal class SamlProtocolMessageSigner(
|
|||
ISamlSigningService samlSigningService,
|
||||
ILogger<SamlProtocolMessageSigner> logger)
|
||||
{
|
||||
internal async Task<string> SignProtocolMessage(XElement messageElement, SamlServiceProvider serviceProvider)
|
||||
internal async Task<string> SignProtocolMessage(XElement messageElement, SamlServiceProvider serviceProvider, Ct ct)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(messageElement);
|
||||
ArgumentNullException.ThrowIfNull(serviceProvider);
|
||||
|
||||
var certificate = await samlSigningService.GetSigningCertificateAsync();
|
||||
var certificate = await samlSigningService.GetSigningCertificateAsync(ct);
|
||||
|
||||
logger.SigningSamlProtocolMessage(LogLevel.Debug, serviceProvider.EntityId, messageElement.Name.LocalName);
|
||||
|
||||
|
|
@ -38,9 +38,9 @@ internal class SamlProtocolMessageSigner(
|
|||
}
|
||||
}
|
||||
|
||||
internal async Task<string> SignQueryString(string queryString)
|
||||
internal async Task<string> SignQueryString(string queryString, Ct ct)
|
||||
{
|
||||
var certificate = await samlSigningService.GetSigningCertificateAsync();
|
||||
var certificate = await samlSigningService.GetSigningCertificateAsync(ct);
|
||||
using var rsa = certificate.GetRSAPrivateKey();
|
||||
if (rsa == null)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -28,9 +28,9 @@ internal abstract class SamlRequestProcessorBase<TMessage, TRequest, TSuccess>(
|
|||
protected readonly ILogger Logger = logger;
|
||||
protected readonly string ExpectedDestination = expectedDestination;
|
||||
|
||||
internal async Task<Result<TSuccess, SamlRequestError<TRequest>>> ProcessAsync(TRequest request, CT ct = default)
|
||||
internal async Task<Result<TSuccess, SamlRequestError<TRequest>>> ProcessAsync(TRequest request, Ct ct = default)
|
||||
{
|
||||
var sp = await ServiceProviderStore.FindByEntityIdAsync(request.Request.Issuer);
|
||||
var sp = await ServiceProviderStore.FindByEntityIdAsync(request.Request.Issuer, ct);
|
||||
if (sp?.Enabled != true)
|
||||
{
|
||||
Logger.ServiceProviderNotFound(LogLevel.Warning, request.Request.Issuer);
|
||||
|
|
@ -140,5 +140,5 @@ internal abstract class SamlRequestProcessorBase<TMessage, TRequest, TSuccess>(
|
|||
return null;
|
||||
}
|
||||
protected abstract SamlRequestError<TRequest>? ValidateMessageSpecific(SamlServiceProvider sp, TRequest request);
|
||||
protected abstract Task<Result<TSuccess, SamlRequestError<TRequest>>> ProcessValidatedRequestAsync(SamlServiceProvider sp, TRequest request, CT ct = default);
|
||||
protected abstract Task<Result<TSuccess, SamlRequestError<TRequest>>> ProcessValidatedRequestAsync(SamlServiceProvider sp, TRequest request, Ct ct = default);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ internal class SamlResponseSigner(
|
|||
IOptions<SamlOptions> samlOptions,
|
||||
ILogger<SamlResponseSigner> logger)
|
||||
{
|
||||
internal async Task<string> SignResponse(XElement responseElement, SamlServiceProvider serviceProvider)
|
||||
internal async Task<string> SignResponse(XElement responseElement, SamlServiceProvider serviceProvider, Ct ct)
|
||||
{
|
||||
var signingBehavior = serviceProvider.SigningBehavior ?? samlOptions.Value.DefaultSigningBehavior;
|
||||
|
||||
|
|
@ -24,7 +24,7 @@ internal class SamlResponseSigner(
|
|||
return responseElement.ToString(SaveOptions.DisableFormatting);
|
||||
}
|
||||
|
||||
var certificate = await samlSigningService.GetSigningCertificateAsync();
|
||||
var certificate = await samlSigningService.GetSigningCertificateAsync(ct);
|
||||
|
||||
logger.SigningSamlResponse(LogLevel.Debug, serviceProvider.EntityId, signingBehavior);
|
||||
|
||||
|
|
|
|||
|
|
@ -18,9 +18,9 @@ internal class SamlSigningService(
|
|||
ILogger<SamlSigningService> logger) : ISamlSigningService
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public async Task<X509Certificate2> GetSigningCertificateAsync()
|
||||
public async Task<X509Certificate2> GetSigningCertificateAsync(Ct ct)
|
||||
{
|
||||
var credential = await GetSigningCredentialsAsync();
|
||||
var credential = await GetSigningCredentialsAsync(ct);
|
||||
if (!TryExtractCertificateFromCredential(credential, out var certificate))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
|
|
@ -37,9 +37,9 @@ internal class SamlSigningService(
|
|||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<string> GetSigningCertificateBase64Async()
|
||||
public async Task<string> GetSigningCertificateBase64Async(Ct ct)
|
||||
{
|
||||
var credential = await GetSigningCredentialsAsync();
|
||||
var credential = await GetSigningCredentialsAsync(ct);
|
||||
if (TryExtractCertificateFromCredential(credential, out var certificate))
|
||||
{
|
||||
var certBytes = certificate.Export(X509ContentType.Cert);
|
||||
|
|
@ -50,9 +50,9 @@ internal class SamlSigningService(
|
|||
"Signing credential key is not an X509SecurityKey and cannot be used to extract an X509 certificate for SAML metadata.");
|
||||
}
|
||||
|
||||
private async Task<SigningCredentials> GetSigningCredentialsAsync()
|
||||
private async Task<SigningCredentials> GetSigningCredentialsAsync(Ct ct)
|
||||
{
|
||||
var credential = await keyMaterialService.GetSigningCredentialsAsync();
|
||||
var credential = await keyMaterialService.GetSigningCredentialsAsync(null, ct);
|
||||
return credential ?? throw new InvalidOperationException("No signing credential available. Configure a signing certificate.");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -32,10 +32,10 @@ internal class SamlMetaDataEndpoint(
|
|||
}
|
||||
|
||||
var options = samlOptions.Value;
|
||||
var issuerUri = await issuerNameService.GetCurrentAsync();
|
||||
var issuerUri = await issuerNameService.GetCurrentAsync(context.RequestAborted);
|
||||
var baseUrl = urls.BaseUrl;
|
||||
|
||||
var certificateBase64 = await samlSigningService.GetSigningCertificateBase64Async();
|
||||
var certificateBase64 = await samlSigningService.GetSigningCertificateBase64Async(context.RequestAborted);
|
||||
|
||||
var singleSignOnService = BuildServiceUrl(baseUrl, options.UserInteraction.Route, options.UserInteraction.SignInPath);
|
||||
var singleLogoutService = BuildServiceUrl(baseUrl, options.UserInteraction.Route, options.UserInteraction.SingleLogoutPath);
|
||||
|
|
|
|||
|
|
@ -10,6 +10,6 @@ namespace Duende.IdentityServer.Internal.Saml;
|
|||
|
||||
internal class NopSamlLogoutNotificationService : ISamlLogoutNotificationService
|
||||
{
|
||||
public Task<IEnumerable<ISamlFrontChannelLogout>> GetSamlFrontChannelLogoutsAsync(LogoutNotificationContext context) =>
|
||||
public Task<IEnumerable<ISamlFrontChannelLogout>> GetSamlFrontChannelLogoutsAsync(LogoutNotificationContext context, Ct ct) =>
|
||||
Task.FromResult(Enumerable.Empty<ISamlFrontChannelLogout>());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ internal class SamlClaimsService(
|
|||
IOptions<SamlOptions> options,
|
||||
ISamlClaimsMapper? customMapper = null)
|
||||
{
|
||||
private async Task<IEnumerable<Claim>> GetClaimsAsync(ClaimsPrincipal user, SamlServiceProvider serviceProvider)
|
||||
private async Task<IEnumerable<Claim>> GetClaimsAsync(ClaimsPrincipal user, SamlServiceProvider serviceProvider, Ct ct)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(user);
|
||||
ArgumentNullException.ThrowIfNull(serviceProvider);
|
||||
|
|
@ -32,13 +32,13 @@ internal class SamlClaimsService(
|
|||
Subject = user,
|
||||
Client = new Client
|
||||
{
|
||||
ClientId = serviceProvider.EntityId.ToString()
|
||||
ClientId = serviceProvider.EntityId
|
||||
},
|
||||
RequestedClaimTypes = requestedClaimTypes,
|
||||
Caller = "SAML"
|
||||
};
|
||||
|
||||
await profileService.GetProfileDataAsync(context);
|
||||
await profileService.GetProfileDataAsync(context, ct);
|
||||
|
||||
var claims = context.IssuedClaims;
|
||||
|
||||
|
|
@ -49,12 +49,13 @@ internal class SamlClaimsService(
|
|||
|
||||
internal async Task<IEnumerable<SamlAttribute>> GetMappedAttributesAsync(
|
||||
ClaimsPrincipal user,
|
||||
SamlServiceProvider serviceProvider)
|
||||
SamlServiceProvider serviceProvider,
|
||||
Ct ct)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(user);
|
||||
ArgumentNullException.ThrowIfNull(serviceProvider);
|
||||
|
||||
var claims = await GetClaimsAsync(user, serviceProvider);
|
||||
var claims = await GetClaimsAsync(user, serviceProvider, ct);
|
||||
|
||||
if (customMapper != null)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -133,16 +133,17 @@ internal class SamlResponseBuilder(
|
|||
ClaimsPrincipal user,
|
||||
SamlServiceProvider samlServiceProvider,
|
||||
SamlAuthenticationState samlAuthenticationState,
|
||||
string sessionIndex)
|
||||
string sessionIndex,
|
||||
Ct ct)
|
||||
{
|
||||
var now = timeProvider.GetUtcNow().DateTime;
|
||||
var options = samlOptions.Value;
|
||||
var nameId = nameIdGenerator.GenerateNameIdentifier(user, samlServiceProvider, samlAuthenticationState.Request);
|
||||
var attributes = await samlClaimsService.GetMappedAttributesAsync(user, samlServiceProvider);
|
||||
var attributes = await samlClaimsService.GetMappedAttributesAsync(user, samlServiceProvider, ct);
|
||||
|
||||
var acsUrl = GetAcsUrl(samlAuthenticationState.Request, samlServiceProvider);
|
||||
|
||||
var issuer = await issuerNameService.GetCurrentAsync();
|
||||
var issuer = await issuerNameService.GetCurrentAsync(ct);
|
||||
|
||||
return new SamlResponse
|
||||
{
|
||||
|
|
|
|||
|
|
@ -17,9 +17,10 @@ internal class LogoutResponseBuilder(
|
|||
internal async Task<LogoutResponse> BuildSuccessResponseAsync(
|
||||
string logoutRequestId,
|
||||
SamlServiceProvider serviceProvider,
|
||||
string? relayState)
|
||||
string? relayState,
|
||||
Ct ct)
|
||||
{
|
||||
var issuer = await issuerNameService.GetCurrentAsync();
|
||||
var issuer = await issuerNameService.GetCurrentAsync(ct);
|
||||
var destination = serviceProvider.SingleLogoutServiceUrl ?? throw new InvalidOperationException("No SingleLogout service url configured");
|
||||
|
||||
return new LogoutResponse
|
||||
|
|
@ -41,9 +42,10 @@ internal class LogoutResponseBuilder(
|
|||
internal async Task<LogoutResponse> BuildErrorResponseAsync(
|
||||
SamlLogoutRequest request,
|
||||
SamlServiceProvider serviceProvider,
|
||||
SamlError error)
|
||||
SamlError error,
|
||||
Ct ct)
|
||||
{
|
||||
var issuer = await issuerNameService.GetCurrentAsync();
|
||||
var issuer = await issuerNameService.GetCurrentAsync(ct);
|
||||
var destination = serviceProvider.SingleLogoutServiceUrl ?? throw new InvalidOperationException("No SingleLogout service url configured");
|
||||
|
||||
return new LogoutResponse
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ internal class LogoutResponse : EndpointResult<LogoutResponse>
|
|||
{
|
||||
var responseXml = serializer.Serialize(result);
|
||||
|
||||
var signedResponseXml = await samlProtocolMessageSigner.SignProtocolMessage(responseXml, result.ServiceProvider);
|
||||
var signedResponseXml = await samlProtocolMessageSigner.SignProtocolMessage(responseXml, result.ServiceProvider, httpContext.RequestAborted);
|
||||
|
||||
var encodedResponse = Convert.ToBase64String(Encoding.UTF8.GetBytes(signedResponseXml));
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,8 @@ internal class SamlFrontChannelLogoutRequestBuilder(
|
|||
string nameId,
|
||||
string? nameIdFormat,
|
||||
string sessionIndex,
|
||||
string issuer)
|
||||
string issuer,
|
||||
Ct ct)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(serviceProvider);
|
||||
|
||||
|
|
@ -48,8 +49,8 @@ internal class SamlFrontChannelLogoutRequestBuilder(
|
|||
|
||||
return serviceProvider.SingleLogoutServiceUrl.Binding switch
|
||||
{
|
||||
SamlBinding.HttpRedirect => await BuildRedirectLogoutRequest(serviceProvider.SingleLogoutServiceUrl.Location, requestXml),
|
||||
SamlBinding.HttpPost => await BuildHttpPostLogoutRequest(serviceProvider, requestXml),
|
||||
SamlBinding.HttpRedirect => await BuildRedirectLogoutRequest(serviceProvider.SingleLogoutServiceUrl.Location, requestXml, ct),
|
||||
SamlBinding.HttpPost => await BuildHttpPostLogoutRequest(serviceProvider, requestXml, ct),
|
||||
_ => throw new InvalidOperationException(
|
||||
$"Binding '{serviceProvider.SingleLogoutServiceUrl.Binding}' is not supported")
|
||||
};
|
||||
|
|
@ -106,13 +107,13 @@ internal class SamlFrontChannelLogoutRequestBuilder(
|
|||
return requestElement;
|
||||
}
|
||||
|
||||
private async Task<ISamlFrontChannelLogout> BuildRedirectLogoutRequest(Uri singleLogoutServiceUri, XElement requestXml)
|
||||
private async Task<ISamlFrontChannelLogout> BuildRedirectLogoutRequest(Uri singleLogoutServiceUri, XElement requestXml, Ct ct)
|
||||
{
|
||||
var encodedRequest = DeflateAndEncode(requestXml.ToString());
|
||||
|
||||
var queryString = $"?SAMLRequest={Uri.EscapeDataString(encodedRequest)}";
|
||||
|
||||
var signedQueryString = await samlProtocolMessageSigner.SignQueryString(queryString);
|
||||
var signedQueryString = await samlProtocolMessageSigner.SignQueryString(queryString, ct);
|
||||
|
||||
return new SamlHttpRedirectFrontChannelLogout(singleLogoutServiceUri, signedQueryString);
|
||||
}
|
||||
|
|
@ -130,9 +131,9 @@ internal class SamlFrontChannelLogoutRequestBuilder(
|
|||
return Convert.ToBase64String(output.ToArray());
|
||||
}
|
||||
|
||||
private async Task<ISamlFrontChannelLogout> BuildHttpPostLogoutRequest(SamlServiceProvider serviceProvider, XElement requestXml)
|
||||
private async Task<ISamlFrontChannelLogout> BuildHttpPostLogoutRequest(SamlServiceProvider serviceProvider, XElement requestXml, Ct ct)
|
||||
{
|
||||
var signedRequestXml = await samlProtocolMessageSigner.SignProtocolMessage(requestXml, serviceProvider);
|
||||
var signedRequestXml = await samlProtocolMessageSigner.SignProtocolMessage(requestXml, serviceProvider, ct);
|
||||
|
||||
var encodedXml = Convert.ToBase64String(Encoding.UTF8.GetBytes(signedRequestXml));
|
||||
|
||||
|
|
|
|||
|
|
@ -18,9 +18,9 @@ internal class SamlLogoutCallbackProcessor(
|
|||
LogoutResponseBuilder logoutResponseBuilder,
|
||||
ILogger<SamlLogoutCallbackProcessor> logger)
|
||||
{
|
||||
internal async Task<Result<LogoutResponse, SamlLogoutCallbackError>> ProcessAsync(string logoutId, CT ct = default)
|
||||
internal async Task<Result<LogoutResponse, SamlLogoutCallbackError>> ProcessAsync(string logoutId, Ct ct = default)
|
||||
{
|
||||
var logoutMessage = await logoutMessageStore.ReadAsync(logoutId);
|
||||
var logoutMessage = await logoutMessageStore.ReadAsync(logoutId, ct);
|
||||
if (logoutMessage?.Data == null)
|
||||
{
|
||||
logger.NoLogoutMessageFound(LogLevel.Warning, logoutId);
|
||||
|
|
@ -36,7 +36,7 @@ internal class SamlLogoutCallbackProcessor(
|
|||
|
||||
logger.BuildingLogoutResponseForSp(LogLevel.Debug, data.SamlServiceProviderEntityId);
|
||||
|
||||
var sp = await serviceProviderStore.FindByEntityIdAsync(data.SamlServiceProviderEntityId);
|
||||
var sp = await serviceProviderStore.FindByEntityIdAsync(data.SamlServiceProviderEntityId, ct);
|
||||
if (sp == null)
|
||||
{
|
||||
logger.ServiceProviderNotFound(LogLevel.Error, data.SamlServiceProviderEntityId);
|
||||
|
|
@ -64,7 +64,8 @@ internal class SamlLogoutCallbackProcessor(
|
|||
var response = await logoutResponseBuilder.BuildSuccessResponseAsync(
|
||||
data.SamlLogoutRequestId,
|
||||
sp,
|
||||
data.SamlRelayState);
|
||||
data.SamlRelayState,
|
||||
ct);
|
||||
|
||||
logger.SuccessfullyBuiltLogoutResponse(LogLevel.Information, data.SamlServiceProviderEntityId, data.SamlLogoutRequestId);
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ internal class SamlLogoutNotificationService(
|
|||
SamlFrontChannelLogoutRequestBuilder frontChannelLogoutRequestBuilder,
|
||||
ILogger<SamlLogoutNotificationService> logger) : ISamlLogoutNotificationService
|
||||
{
|
||||
public async Task<IEnumerable<ISamlFrontChannelLogout>> GetSamlFrontChannelLogoutsAsync(LogoutNotificationContext context)
|
||||
public async Task<IEnumerable<ISamlFrontChannelLogout>> GetSamlFrontChannelLogoutsAsync(LogoutNotificationContext context, Ct ct)
|
||||
{
|
||||
using var activity = Tracing.ServiceActivitySource.StartActivity("LogoutNotificationService.GetSamlFrontChannelLogoutUrls");
|
||||
|
||||
|
|
@ -27,11 +27,11 @@ internal class SamlLogoutNotificationService(
|
|||
return logoutUrls;
|
||||
}
|
||||
|
||||
var issuer = await issuerNameService.GetCurrentAsync();
|
||||
var issuer = await issuerNameService.GetCurrentAsync(ct);
|
||||
|
||||
foreach (var sessionData in context.SamlSessions ?? [])
|
||||
{
|
||||
var sp = await serviceProviderStore.FindByEntityIdAsync(sessionData.EntityId);
|
||||
var sp = await serviceProviderStore.FindByEntityIdAsync(sessionData.EntityId, ct);
|
||||
if (sp?.Enabled != true)
|
||||
{
|
||||
logger.SkippingLogoutUrlGenerationForUnknownOrDisabledServiceProvider(LogLevel.Debug, sessionData.EntityId);
|
||||
|
|
@ -51,7 +51,8 @@ internal class SamlLogoutNotificationService(
|
|||
sessionData.NameId,
|
||||
sessionData.NameIdFormat,
|
||||
sessionData.SessionIndex,
|
||||
issuer);
|
||||
issuer,
|
||||
ct);
|
||||
|
||||
logoutUrls.Add(logoutUrl);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ internal class SamlLogoutRequestProcessor : SamlRequestProcessorBase<LogoutReque
|
|||
protected override async Task<Result<SamlLogoutSuccess, SamlRequestError<SamlLogoutRequest>>> ProcessValidatedRequestAsync(
|
||||
SamlServiceProvider sp,
|
||||
SamlLogoutRequest request,
|
||||
CT ct = default)
|
||||
Ct ct = default)
|
||||
{
|
||||
var logoutRequest = request.LogoutRequest;
|
||||
|
||||
|
|
@ -71,27 +71,27 @@ internal class SamlLogoutRequestProcessor : SamlRequestProcessorBase<LogoutReque
|
|||
|
||||
Logger.ProcessingSamlLogoutRequest(LogLevel.Debug, logoutRequest.Id, sp.DisplayName, logoutRequest.Issuer);
|
||||
|
||||
var user = await _userSession.GetUserAsync();
|
||||
var user = await _userSession.GetUserAsync(ct);
|
||||
if (user == null)
|
||||
{
|
||||
Logger.SamlLogoutRequestReceivedButNoActiveUserSession(LogLevel.Debug, logoutRequest.Id, logoutRequest.Issuer);
|
||||
var noUserAuthenticatedResponse = await _logoutResponseBuilder.BuildSuccessResponseAsync(logoutRequest.Id, sp, request.RelayState);
|
||||
var noUserAuthenticatedResponse = await _logoutResponseBuilder.BuildSuccessResponseAsync(logoutRequest.Id, sp, request.RelayState, ct);
|
||||
// there is no user to log out, return success
|
||||
return SamlLogoutSuccess.CreateResponse(noUserAuthenticatedResponse);
|
||||
}
|
||||
|
||||
var sessionMatch = await ValidateSessionIndexAsync(sp, logoutRequest.SessionIndex);
|
||||
var sessionMatch = await ValidateSessionIndexAsync(sp, logoutRequest.SessionIndex, ct);
|
||||
if (!sessionMatch)
|
||||
{
|
||||
Logger.SamlLogoutRequestReceivedWithWrongSessionIndex(LogLevel.Warning, logoutRequest.Id, logoutRequest.SessionIndex);
|
||||
var noSessionIndexResponse = await _logoutResponseBuilder.BuildSuccessResponseAsync(logoutRequest.Id, sp, request.RelayState);
|
||||
var noSessionIndexResponse = await _logoutResponseBuilder.BuildSuccessResponseAsync(logoutRequest.Id, sp, request.RelayState, ct);
|
||||
// there is no session to terminate, return success
|
||||
return SamlLogoutSuccess.CreateResponse(noSessionIndexResponse);
|
||||
}
|
||||
|
||||
Logger.SamlLogoutRedirectToLogoutPage(LogLevel.Information, logoutRequest.Issuer);
|
||||
|
||||
var logoutId = await StoreLogoutMessageAsync(user, sp, request);
|
||||
var logoutId = await StoreLogoutMessageAsync(user, sp, request, ct);
|
||||
var logoutUri = _urlBuilder.SamlLogoutUri(logoutId);
|
||||
|
||||
return SamlLogoutSuccess.CreateRedirect(logoutUri);
|
||||
|
|
@ -129,11 +129,11 @@ internal class SamlLogoutRequestProcessor : SamlRequestProcessorBase<LogoutReque
|
|||
return null;
|
||||
}
|
||||
|
||||
private async Task<bool> ValidateSessionIndexAsync(SamlServiceProvider sp, string sessionIndex)
|
||||
private async Task<bool> ValidateSessionIndexAsync(SamlServiceProvider sp, string sessionIndex, Ct ct)
|
||||
{
|
||||
var samlSessions = await _userSession.GetSamlSessionListAsync();
|
||||
var samlSessions = await _userSession.GetSamlSessionListAsync(ct);
|
||||
|
||||
var spSession = samlSessions.FirstOrDefault(s => s.EntityId == sp.EntityId.ToString());
|
||||
var spSession = samlSessions.FirstOrDefault(s => s.EntityId == sp.EntityId);
|
||||
|
||||
if (spSession == null)
|
||||
{
|
||||
|
|
@ -150,16 +150,16 @@ internal class SamlLogoutRequestProcessor : SamlRequestProcessorBase<LogoutReque
|
|||
return true;
|
||||
}
|
||||
|
||||
private async Task<string> StoreLogoutMessageAsync(ClaimsPrincipal user, SamlServiceProvider serviceProvider, SamlLogoutRequest logoutRequest)
|
||||
private async Task<string> StoreLogoutMessageAsync(ClaimsPrincipal user, SamlServiceProvider serviceProvider, SamlLogoutRequest logoutRequest, Ct ct)
|
||||
{
|
||||
var samlSessions = await _userSession.GetSamlSessionListAsync();
|
||||
var samlSessions = await _userSession.GetSamlSessionListAsync(ct);
|
||||
|
||||
var oidcClientIds = await _userSession.GetClientListAsync();
|
||||
var oidcClientIds = await _userSession.GetClientListAsync(ct);
|
||||
|
||||
var logoutMessage = new LogoutMessage
|
||||
{
|
||||
SubjectId = user.GetSubjectId(),
|
||||
SessionId = await _userSession.GetSessionIdAsync(),
|
||||
SessionId = await _userSession.GetSessionIdAsync(ct),
|
||||
ClientIds = oidcClientIds,
|
||||
SamlServiceProviderEntityId = serviceProvider.EntityId,
|
||||
SamlSessions = samlSessions,
|
||||
|
|
@ -170,6 +170,6 @@ internal class SamlLogoutRequestProcessor : SamlRequestProcessorBase<LogoutReque
|
|||
|
||||
var msg = new Message<LogoutMessage>(logoutMessage, _timeProvider.GetUtcNow().UtcDateTime);
|
||||
|
||||
return await _logoutMessageStore.WriteAsync(msg);
|
||||
return await _logoutMessageStore.WriteAsync(msg, ct);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ internal class SamlSingleLogoutEndpoint(
|
|||
return await ProcessLogoutRequest(logoutRequest, context.RequestAborted);
|
||||
}
|
||||
|
||||
internal async Task<IEndpointResult> ProcessLogoutRequest(SamlLogoutRequest logoutRequest, CT ct = default)
|
||||
internal async Task<IEndpointResult> ProcessLogoutRequest(SamlLogoutRequest logoutRequest, Ct ct = default)
|
||||
{
|
||||
logger.ReceivedLogoutRequest(LogLevel.Debug, logoutRequest.LogoutRequest.Issuer, logoutRequest.LogoutRequest.Id, logoutRequest.LogoutRequest.SessionIndex);
|
||||
|
||||
|
|
@ -44,7 +44,7 @@ internal class SamlSingleLogoutEndpoint(
|
|||
return error.Type switch
|
||||
{
|
||||
SamlRequestErrorType.Validation => HandleValidationError(error),
|
||||
SamlRequestErrorType.Protocol => await HandleProtocolError(error),
|
||||
SamlRequestErrorType.Protocol => await HandleProtocolError(error, ct),
|
||||
_ => throw new InvalidOperationException($"Unexpected error type: {error.Type}")
|
||||
};
|
||||
}
|
||||
|
|
@ -61,7 +61,7 @@ internal class SamlSingleLogoutEndpoint(
|
|||
return new ValidationProblemResult(error.ValidationMessage!);
|
||||
}
|
||||
|
||||
private async Task<LogoutResponse> HandleProtocolError(SamlRequestError<SamlLogoutRequest> error)
|
||||
private async Task<LogoutResponse> HandleProtocolError(SamlRequestError<SamlLogoutRequest> error, Ct ct)
|
||||
{
|
||||
var protocolError = error.ProtocolError!;
|
||||
logger.SamlLogoutProtocolError(LogLevel.Information,
|
||||
|
|
@ -71,6 +71,7 @@ internal class SamlSingleLogoutEndpoint(
|
|||
return await responseBuilder.BuildErrorResponseAsync(
|
||||
protocolError.Request,
|
||||
protocolError.ServiceProvider,
|
||||
protocolError.Error);
|
||||
protocolError.Error,
|
||||
ct);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,9 +18,9 @@ internal class DefaultSamlSigninInteractionResponseGenerator(
|
|||
IHttpContextAccessor httpContextAccessor)
|
||||
: ISamlSigninInteractionResponseGenerator
|
||||
{
|
||||
public async Task<SamlInteractionResponse> ProcessInteractionAsync(SamlServiceProvider sp, AuthNRequest request, CT ct = default)
|
||||
public async Task<SamlInteractionResponse> ProcessInteractionAsync(SamlServiceProvider sp, AuthNRequest request, Ct ct = default)
|
||||
{
|
||||
var signedInUser = await userSession.GetUserAsync();
|
||||
var signedInUser = await userSession.GetUserAsync(ct);
|
||||
|
||||
if (signedInUser != null)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ internal class DistributedCacheSamlSigninStateStore(IDistributedCache cache) : I
|
|||
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10)
|
||||
};
|
||||
|
||||
public async Task<StateId> StoreSigninRequestStateAsync(SamlAuthenticationState state, CT ct = default)
|
||||
public async Task<StateId> StoreSigninRequestStateAsync(SamlAuthenticationState state, Ct ct = default)
|
||||
{
|
||||
var stateId = StateId.NewId();
|
||||
var key = GetKey(stateId);
|
||||
|
|
@ -27,7 +27,7 @@ internal class DistributedCacheSamlSigninStateStore(IDistributedCache cache) : I
|
|||
return stateId;
|
||||
}
|
||||
|
||||
public async Task<SamlAuthenticationState?> RetrieveSigninRequestStateAsync(StateId stateId, CT ct = default)
|
||||
public async Task<SamlAuthenticationState?> RetrieveSigninRequestStateAsync(StateId stateId, Ct ct = default)
|
||||
{
|
||||
var key = GetKey(stateId);
|
||||
var json = await cache.GetStringAsync(key, ct);
|
||||
|
|
@ -42,7 +42,7 @@ internal class DistributedCacheSamlSigninStateStore(IDistributedCache cache) : I
|
|||
return JsonSerializer.Deserialize<SamlAuthenticationState>(json);
|
||||
}
|
||||
|
||||
public async Task UpdateSigninRequestStateAsync(StateId stateId, SamlAuthenticationState state, CT ct = default)
|
||||
public async Task UpdateSigninRequestStateAsync(StateId stateId, SamlAuthenticationState state, Ct ct = default)
|
||||
{
|
||||
var key = GetKey(stateId);
|
||||
var json = JsonSerializer.Serialize(state);
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ namespace Duende.IdentityServer.Internal.Saml.SingleSignin;
|
|||
|
||||
internal interface ISamlSigninStateStore
|
||||
{
|
||||
Task<StateId> StoreSigninRequestStateAsync(SamlAuthenticationState request, CT ct = default);
|
||||
Task<SamlAuthenticationState?> RetrieveSigninRequestStateAsync(StateId stateId, CT ct = default);
|
||||
Task UpdateSigninRequestStateAsync(StateId stateId, SamlAuthenticationState state, CT ct = default);
|
||||
Task<StateId> StoreSigninRequestStateAsync(SamlAuthenticationState request, Ct ct = default);
|
||||
Task<SamlAuthenticationState?> RetrieveSigninRequestStateAsync(StateId stateId, Ct ct = default);
|
||||
Task UpdateSigninRequestStateAsync(StateId stateId, SamlAuthenticationState state, Ct ct = default);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ internal class SamlResponse : EndpointResult<SamlResponse>
|
|||
{
|
||||
var responseXml = serializer.Serialize(result);
|
||||
|
||||
var signedResponseXml = await samlResponseSigner.SignResponse(responseXml, result.ServiceProvider);
|
||||
var signedResponseXml = await samlResponseSigner.SignResponse(responseXml, result.ServiceProvider, httpContext.RequestAborted);
|
||||
|
||||
if (result.ServiceProvider.EncryptAssertions)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ internal class SamlIdpInitiatedEndpoint(
|
|||
internal async Task<IEndpointResult> ProcessInternalAsync(
|
||||
string spEntityId,
|
||||
string? relayState,
|
||||
CT ct = default)
|
||||
Ct ct = default)
|
||||
{
|
||||
logger.StartIdpInitiatedRequest(LogLevel.Debug, spEntityId);
|
||||
|
||||
|
|
|
|||
|
|
@ -24,9 +24,9 @@ internal class SamlIdpInitiatedRequestProcessor(
|
|||
internal async Task<Result<SamlSigninSuccess, SamlRequestError<SamlSigninRequest>>> ProcessAsync(
|
||||
string spEntityId,
|
||||
string? relayState,
|
||||
CT ct = default)
|
||||
Ct ct = default)
|
||||
{
|
||||
var sp = await serviceProviderStore.FindByEntityIdAsync(spEntityId);
|
||||
var sp = await serviceProviderStore.FindByEntityIdAsync(spEntityId, ct);
|
||||
if (sp == null)
|
||||
{
|
||||
return new SamlRequestError<SamlSigninRequest>
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ internal class SamlSigninCallbackEndpoint(SamlResponseBuilder responseBuilder, S
|
|||
return await Process(context.RequestAborted);
|
||||
}
|
||||
|
||||
internal async Task<IEndpointResult> Process(CT ct = default)
|
||||
internal async Task<IEndpointResult> Process(Ct ct = default)
|
||||
{
|
||||
logger.StartSamlSigninCallbackRequest(LogLevel.Debug);
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ internal class SamlSigninCallbackRequestProcessor(
|
|||
SamlUrlBuilder samlUrlBuilder,
|
||||
SamlResponseBuilder responseBuilder)
|
||||
{
|
||||
internal async Task<Result<SamlSigninSuccess, SamlRequestError<SamlSigninRequest>>> ProcessAsync(CT ct = default)
|
||||
internal async Task<Result<SamlSigninSuccess, SamlRequestError<SamlSigninRequest>>> ProcessAsync(Ct ct = default)
|
||||
{
|
||||
if (!stateIdCookie.TryGetSamlSigninStateId(out var stateId))
|
||||
{
|
||||
|
|
@ -39,7 +39,7 @@ internal class SamlSigninCallbackRequestProcessor(
|
|||
};
|
||||
}
|
||||
|
||||
var user = await userSession.GetUserAsync();
|
||||
var user = await userSession.GetUserAsync(ct);
|
||||
if (user == null || !user.IsAuthenticated())
|
||||
{
|
||||
var loginUri = samlUrlBuilder.SamlLoginUri();
|
||||
|
|
@ -48,7 +48,7 @@ internal class SamlSigninCallbackRequestProcessor(
|
|||
}
|
||||
|
||||
var samlServiceProvider =
|
||||
await serviceProviderStore.FindByEntityIdAsync(authenticationState.ServiceProviderEntityId);
|
||||
await serviceProviderStore.FindByEntityIdAsync(authenticationState.ServiceProviderEntityId, ct);
|
||||
|
||||
if (samlServiceProvider is not { Enabled: true })
|
||||
{
|
||||
|
|
@ -61,7 +61,7 @@ internal class SamlSigninCallbackRequestProcessor(
|
|||
}
|
||||
|
||||
// Check if this SP already has a session - if so, reuse the SessionIndex
|
||||
var existingSessions = await userSession.GetSamlSessionListAsync();
|
||||
var existingSessions = await userSession.GetSamlSessionListAsync(ct);
|
||||
var existingSession = existingSessions.FirstOrDefault(s => s.EntityId == samlServiceProvider.EntityId);
|
||||
string sessionIndex;
|
||||
|
||||
|
|
@ -76,7 +76,7 @@ internal class SamlSigninCallbackRequestProcessor(
|
|||
sessionIndex = Guid.NewGuid().ToString("N");
|
||||
}
|
||||
|
||||
var samlResponse = await responseBuilder.BuildSuccessResponseAsync(user, samlServiceProvider, authenticationState, sessionIndex);
|
||||
var samlResponse = await responseBuilder.BuildSuccessResponseAsync(user, samlServiceProvider, authenticationState, sessionIndex, ct);
|
||||
|
||||
if (string.IsNullOrEmpty(samlResponse.Assertion?.Subject?.NameId?.Value))
|
||||
{
|
||||
|
|
@ -96,7 +96,7 @@ internal class SamlSigninCallbackRequestProcessor(
|
|||
NameId = samlResponse.Assertion.Subject.NameId.Value,
|
||||
NameIdFormat = samlResponse.Assertion.Subject.NameId.Format
|
||||
};
|
||||
await userSession.AddSamlSessionAsync(sessionData);
|
||||
await userSession.AddSamlSessionAsync(sessionData, ct);
|
||||
|
||||
stateIdCookie.ClearAuthenticationState();
|
||||
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ internal class SamlSigninEndpoint(
|
|||
|
||||
internal async Task<IEndpointResult> ProcessSpInitiatedSignin(
|
||||
SamlSigninRequest signinRequest,
|
||||
CT ct = default)
|
||||
Ct ct = default)
|
||||
{
|
||||
logger.StartSamlSigninRequest(LogLevel.Debug);
|
||||
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ internal class SamlSigninRequestProcessor(
|
|||
protected override async Task<Result<SamlSigninSuccess, SamlRequestError<SamlSigninRequest>>> ProcessValidatedRequestAsync(
|
||||
SamlServiceProvider sp,
|
||||
SamlSigninRequest signinRequest,
|
||||
CT ct = default)
|
||||
Ct ct = default)
|
||||
{
|
||||
var authNRequest = signinRequest.AuthNRequest;
|
||||
|
||||
|
|
@ -178,7 +178,7 @@ internal class SamlSigninRequestProcessor(
|
|||
Uri assertionConsumerServiceUrl,
|
||||
AuthNRequest authNRequest,
|
||||
SamlServiceProvider sp,
|
||||
CT ct = default)
|
||||
Ct ct = default)
|
||||
{
|
||||
var state = new SamlAuthenticationState
|
||||
{
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ public interface ISamlInteractionService
|
|||
/// Gets the SAML authentication request context from the current request's state cookie.
|
||||
/// Returns null if no SAML authentication is in progress.
|
||||
/// </summary>
|
||||
Task<SamlAuthenticationRequest?> GetAuthenticationRequestContextAsync(CT ct = default);
|
||||
Task<SamlAuthenticationRequest?> GetAuthenticationRequestContextAsync(Ct ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Stores whether the user met the requirements of the RequestedAuthnContext in the
|
||||
|
|
@ -26,5 +26,5 @@ public interface ISamlInteractionService
|
|||
/// <param name="requestedAuthnContextRequirementsWereMet">Whether the requirements of the RequestedAuthnContext were met.</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
/// <returns></returns>
|
||||
Task StoreRequestedAuthnContextResultAsync(bool requestedAuthnContextRequirementsWereMet, CT ct = default);
|
||||
Task StoreRequestedAuthnContextResultAsync(bool requestedAuthnContextRequirementsWereMet, Ct ct = default);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,5 +7,10 @@ namespace Duende.IdentityServer.Saml;
|
|||
|
||||
public interface ISamlLogoutNotificationService
|
||||
{
|
||||
Task<IEnumerable<ISamlFrontChannelLogout>> GetSamlFrontChannelLogoutsAsync(LogoutNotificationContext context);
|
||||
/// <summary>
|
||||
/// Builds the URLs needed for front-channel logout notification.
|
||||
/// </summary>
|
||||
/// <param name="context">The context for the logout notification.</param>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
Task<IEnumerable<ISamlFrontChannelLogout>> GetSamlFrontChannelLogoutsAsync(LogoutNotificationContext context, Ct ct);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,5 +8,5 @@ namespace Duende.IdentityServer.Saml;
|
|||
|
||||
public interface ISamlSigninInteractionResponseGenerator
|
||||
{
|
||||
Task<SamlInteractionResponse> ProcessInteractionAsync(SamlServiceProvider sp, AuthNRequest request, CT ct = default);
|
||||
Task<SamlInteractionResponse> ProcessInteractionAsync(SamlServiceProvider sp, AuthNRequest request, Ct ct = default);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -364,7 +364,7 @@ public class DefaultUserSession : IUserSession
|
|||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual async Task AddSamlSessionAsync(SamlSpSessionData session)
|
||||
public virtual async Task AddSamlSessionAsync(SamlSpSessionData session, Ct ct)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(session);
|
||||
|
||||
|
|
@ -377,7 +377,7 @@ public class DefaultUserSession : IUserSession
|
|||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual async Task<IEnumerable<SamlSpSessionData>> GetSamlSessionListAsync()
|
||||
public virtual async Task<IEnumerable<SamlSpSessionData>> GetSamlSessionListAsync(Ct ct)
|
||||
{
|
||||
await AuthenticateAsync();
|
||||
|
||||
|
|
@ -397,7 +397,7 @@ public class DefaultUserSession : IUserSession
|
|||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual async Task RemoveSamlSessionAsync(string entityId)
|
||||
public virtual async Task RemoveSamlSessionAsync(string entityId, Ct ct)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(entityId);
|
||||
|
||||
|
|
|
|||
|
|
@ -30,8 +30,9 @@ public class InMemorySamlServiceProviderStore : ISamlServiceProviderStore
|
|||
/// Finds a SAML Service Provider by its entity identifier.
|
||||
/// </summary>
|
||||
/// <param name="entityId">The entity identifier of the Service Provider.</param>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <returns>The Service Provider, or null if not found.</returns>
|
||||
public Task<SamlServiceProvider> FindByEntityIdAsync(string entityId)
|
||||
public Task<SamlServiceProvider> FindByEntityIdAsync(string entityId, Ct ct)
|
||||
{
|
||||
using var activity = Tracing.StoreActivitySource.StartActivity("InMemorySamlServiceProviderStore.FindByEntityId");
|
||||
activity?.SetTag(Tracing.Properties.SamlEntityId, entityId);
|
||||
|
|
|
|||
|
|
@ -251,7 +251,7 @@ public class EndSessionRequestValidator : IEndSessionRequestValidator
|
|||
result.IsError = false;
|
||||
result.FrontChannelLogoutUrls = await LogoutNotificationService.GetFrontChannelLogoutNotificationsUrlsAsync(endSessionMessage.Data, ct);
|
||||
|
||||
var samlFrontChannelLogouts = await SamlLogoutNotificationService.GetSamlFrontChannelLogoutsAsync(endSessionMessage.Data);
|
||||
var samlFrontChannelLogouts = await SamlLogoutNotificationService.GetSamlFrontChannelLogoutsAsync(endSessionMessage.Data, ct);
|
||||
result.SamlFrontChannelLogouts = samlFrontChannelLogouts;
|
||||
}
|
||||
else
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ public interface ISamlServiceProviderStore
|
|||
/// Finds a SAML Service Provider by its entity identifier.
|
||||
/// </summary>
|
||||
/// <param name="entityId">The entity identifier of the Service Provider.</param>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <returns>The Service Provider, or null if not found.</returns>
|
||||
Task<SamlServiceProvider?> FindByEntityIdAsync(string entityId);
|
||||
Task<SamlServiceProvider?> FindByEntityIdAsync(string entityId, Ct ct);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ internal class CookieHandler(HttpMessageHandler innerHandler, CookieContainer? c
|
|||
public void ClearCookies() => CookieContainer = new CookieContainer();
|
||||
public CookieContainer CookieContainer { get; private set; } = cookies ?? new CookieContainer();
|
||||
|
||||
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CT ct)
|
||||
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, Ct ct)
|
||||
{
|
||||
var requestUri = request.RequestUri;
|
||||
var header = CookieContainer.GetCookieHeader(requestUri!);
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ public class SamlClaimsMappingTests
|
|||
{
|
||||
private const string Category = "SAML Claims Mapping";
|
||||
|
||||
private readonly Ct _ct = TestContext.Current.CancellationToken;
|
||||
|
||||
private SamlFixture Fixture = new();
|
||||
private SamlDataBuilder Build => Fixture.Builder;
|
||||
|
||||
|
|
@ -36,17 +38,17 @@ public class SamlClaimsMappingTests
|
|||
};
|
||||
|
||||
Fixture.UserToSignIn = new ClaimsPrincipal(new ClaimsIdentity(claims, "Test"));
|
||||
await Fixture.Client.GetAsync("/__signin", CT.None);
|
||||
await Fixture.Client.GetAsync("/__signin", _ct);
|
||||
|
||||
var authnRequestXml = Build.AuthNRequestXml();
|
||||
var urlEncoded = await EncodeRequest(authnRequestXml, CT.None);
|
||||
var urlEncoded = await EncodeRequest(authnRequestXml, _ct);
|
||||
|
||||
// Act
|
||||
var result = await Fixture.Client.GetAsync($"/saml/signin?SAMLRequest={urlEncoded}", CT.None);
|
||||
var result = await Fixture.Client.GetAsync($"/saml/signin?SAMLRequest={urlEncoded}", _ct);
|
||||
|
||||
// Assert
|
||||
result.StatusCode.ShouldBe(HttpStatusCode.OK);
|
||||
var successResponse = await ExtractSamlSuccessFromPostAsync(result, CT.None);
|
||||
var successResponse = await ExtractSamlSuccessFromPostAsync(result, _ct);
|
||||
|
||||
// Verify mapped attributes are present with correct names
|
||||
var attributes = successResponse.Assertion.Attributes;
|
||||
|
|
@ -82,17 +84,17 @@ public class SamlClaimsMappingTests
|
|||
};
|
||||
|
||||
Fixture.UserToSignIn = new ClaimsPrincipal(new ClaimsIdentity(claims, "Test"));
|
||||
await Fixture.Client.GetAsync("/__signin", CT.None);
|
||||
await Fixture.Client.GetAsync("/__signin", _ct);
|
||||
|
||||
var authnRequestXml = Build.AuthNRequestXml();
|
||||
var urlEncoded = await EncodeRequest(authnRequestXml, CT.None);
|
||||
var urlEncoded = await EncodeRequest(authnRequestXml, _ct);
|
||||
|
||||
// Act
|
||||
var result = await Fixture.Client.GetAsync($"/saml/signin?SAMLRequest={urlEncoded}", CT.None);
|
||||
var result = await Fixture.Client.GetAsync($"/saml/signin?SAMLRequest={urlEncoded}", _ct);
|
||||
|
||||
// Assert
|
||||
result.StatusCode.ShouldBe(HttpStatusCode.OK);
|
||||
var successResponse = await ExtractSamlSuccessFromPostAsync(result, CT.None);
|
||||
var successResponse = await ExtractSamlSuccessFromPostAsync(result, _ct);
|
||||
|
||||
var attributes = successResponse.Assertion.Attributes;
|
||||
attributes.ShouldNotBeNull();
|
||||
|
|
@ -130,17 +132,17 @@ public class SamlClaimsMappingTests
|
|||
};
|
||||
|
||||
Fixture.UserToSignIn = new ClaimsPrincipal(new ClaimsIdentity(claims, "Test"));
|
||||
await Fixture.Client.GetAsync("/__signin", CT.None);
|
||||
await Fixture.Client.GetAsync("/__signin", _ct);
|
||||
|
||||
var authnRequestXml = Build.AuthNRequestXml();
|
||||
var urlEncoded = await EncodeRequest(authnRequestXml, CT.None);
|
||||
var urlEncoded = await EncodeRequest(authnRequestXml, _ct);
|
||||
|
||||
// Act
|
||||
var result = await Fixture.Client.GetAsync($"/saml/signin?SAMLRequest={urlEncoded}", CT.None);
|
||||
var result = await Fixture.Client.GetAsync($"/saml/signin?SAMLRequest={urlEncoded}", _ct);
|
||||
|
||||
// Assert
|
||||
result.StatusCode.ShouldBe(HttpStatusCode.OK);
|
||||
var successResponse = await ExtractSamlSuccessFromPostAsync(result, CT.None);
|
||||
var successResponse = await ExtractSamlSuccessFromPostAsync(result, _ct);
|
||||
|
||||
var attributes = successResponse.Assertion.Attributes;
|
||||
attributes.ShouldNotBeNull();
|
||||
|
|
@ -181,17 +183,17 @@ public class SamlClaimsMappingTests
|
|||
};
|
||||
|
||||
Fixture.UserToSignIn = new ClaimsPrincipal(new ClaimsIdentity(claims, "Test"));
|
||||
await Fixture.Client.GetAsync("/__signin", CT.None);
|
||||
await Fixture.Client.GetAsync("/__signin", _ct);
|
||||
|
||||
var authnRequestXml = Build.AuthNRequestXml();
|
||||
var urlEncoded = await EncodeRequest(authnRequestXml, CT.None);
|
||||
var urlEncoded = await EncodeRequest(authnRequestXml, _ct);
|
||||
|
||||
// Act
|
||||
var result = await Fixture.Client.GetAsync($"/saml/signin?SAMLRequest={urlEncoded}", CT.None);
|
||||
var result = await Fixture.Client.GetAsync($"/saml/signin?SAMLRequest={urlEncoded}", _ct);
|
||||
|
||||
// Assert
|
||||
result.StatusCode.ShouldBe(HttpStatusCode.OK);
|
||||
var successResponse = await ExtractSamlSuccessFromPostAsync(result, CT.None);
|
||||
var successResponse = await ExtractSamlSuccessFromPostAsync(result, _ct);
|
||||
|
||||
var attributes = successResponse.Assertion.Attributes;
|
||||
attributes.ShouldNotBeNull();
|
||||
|
|
@ -223,17 +225,17 @@ public class SamlClaimsMappingTests
|
|||
};
|
||||
|
||||
Fixture.UserToSignIn = new ClaimsPrincipal(new ClaimsIdentity(claims, "Test"));
|
||||
await Fixture.Client.GetAsync("/__signin", CT.None);
|
||||
await Fixture.Client.GetAsync("/__signin", _ct);
|
||||
|
||||
var authnRequestXml = Build.AuthNRequestXml();
|
||||
var urlEncoded = await EncodeRequest(authnRequestXml, CT.None);
|
||||
var urlEncoded = await EncodeRequest(authnRequestXml, _ct);
|
||||
|
||||
// Act
|
||||
var result = await Fixture.Client.GetAsync($"/saml/signin?SAMLRequest={urlEncoded}", CT.None);
|
||||
var result = await Fixture.Client.GetAsync($"/saml/signin?SAMLRequest={urlEncoded}", _ct);
|
||||
|
||||
// Assert
|
||||
result.StatusCode.ShouldBe(HttpStatusCode.OK);
|
||||
var successResponse = await ExtractSamlSuccessFromPostAsync(result, CT.None);
|
||||
var successResponse = await ExtractSamlSuccessFromPostAsync(result, _ct);
|
||||
|
||||
var attributes = successResponse.Assertion.Attributes;
|
||||
attributes.ShouldNotBeNull();
|
||||
|
|
@ -279,17 +281,17 @@ public class SamlClaimsMappingTests
|
|||
};
|
||||
|
||||
Fixture.UserToSignIn = new ClaimsPrincipal(new ClaimsIdentity(claims, "Test"));
|
||||
await Fixture.Client.GetAsync("/__signin", CT.None);
|
||||
await Fixture.Client.GetAsync("/__signin", _ct);
|
||||
|
||||
var authnRequestXml = Build.AuthNRequestXml();
|
||||
var urlEncoded = await EncodeRequest(authnRequestXml, CT.None);
|
||||
var urlEncoded = await EncodeRequest(authnRequestXml, _ct);
|
||||
|
||||
// Act
|
||||
var result = await Fixture.Client.GetAsync($"/saml/signin?SAMLRequest={urlEncoded}", CT.None);
|
||||
var result = await Fixture.Client.GetAsync($"/saml/signin?SAMLRequest={urlEncoded}", _ct);
|
||||
|
||||
// Assert
|
||||
result.StatusCode.ShouldBe(HttpStatusCode.OK);
|
||||
var successResponse = await ExtractSamlSuccessFromPostAsync(result, CT.None);
|
||||
var successResponse = await ExtractSamlSuccessFromPostAsync(result, _ct);
|
||||
|
||||
var attributes = successResponse.Assertion.Attributes;
|
||||
attributes.ShouldNotBeNull();
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ public class SamlEncryptionTests
|
|||
{
|
||||
private const string Category = "SAML Encryption";
|
||||
|
||||
private readonly Ct _ct = TestContext.Current.CancellationToken;
|
||||
|
||||
private SamlFixture Fixture = new();
|
||||
private SamlData Data => Fixture.Data;
|
||||
private SamlDataBuilder Build => Fixture.Builder;
|
||||
|
|
@ -59,16 +61,16 @@ public class SamlEncryptionTests
|
|||
"Test"));
|
||||
|
||||
await Fixture.InitializeAsync();
|
||||
await Fixture.Client.GetAsync("/__signin", CT.None);
|
||||
await Fixture.Client.GetAsync("/__signin", _ct);
|
||||
|
||||
// Act
|
||||
var urlEncoded = await EncodeRequest(Build.AuthNRequestXml(), CT.None);
|
||||
var result = await Fixture.Client.GetAsync($"/saml/signin?SAMLRequest={urlEncoded}", CT.None);
|
||||
var urlEncoded = await EncodeRequest(Build.AuthNRequestXml(), _ct);
|
||||
var result = await Fixture.Client.GetAsync($"/saml/signin?SAMLRequest={urlEncoded}", _ct);
|
||||
|
||||
// Assert
|
||||
result.StatusCode.ShouldBe(HttpStatusCode.OK);
|
||||
|
||||
var responseData = await ExtractSamlResponse(result, CT.None);
|
||||
var responseData = await ExtractSamlResponse(result, _ct);
|
||||
var responseXml = responseData.responseXml;
|
||||
|
||||
// Verify encrypted assertion is present
|
||||
|
|
@ -105,16 +107,16 @@ public class SamlEncryptionTests
|
|||
"Test"));
|
||||
|
||||
await Fixture.InitializeAsync();
|
||||
await Fixture.Client.GetAsync("/__signin", CT.None);
|
||||
await Fixture.Client.GetAsync("/__signin", _ct);
|
||||
|
||||
// Act
|
||||
var urlEncoded = await EncodeRequest(Build.AuthNRequestXml(), CT.None);
|
||||
var result = await Fixture.Client.GetAsync($"/saml/signin?SAMLRequest={urlEncoded}", CT.None);
|
||||
var urlEncoded = await EncodeRequest(Build.AuthNRequestXml(), _ct);
|
||||
var result = await Fixture.Client.GetAsync($"/saml/signin?SAMLRequest={urlEncoded}", _ct);
|
||||
|
||||
// Assert - Decrypt and verify actual content
|
||||
result.StatusCode.ShouldBe(HttpStatusCode.OK);
|
||||
|
||||
var samlResponse = await ExtractAndDecryptSamlSuccessFromPostAsync(result, encryptionCert, CT.None);
|
||||
var samlResponse = await ExtractAndDecryptSamlSuccessFromPostAsync(result, encryptionCert, _ct);
|
||||
|
||||
samlResponse.StatusCode.ShouldBe(SamlStatusCodes.Success);
|
||||
samlResponse.Assertion.ShouldNotBeNull();
|
||||
|
|
@ -163,16 +165,16 @@ public class SamlEncryptionTests
|
|||
"Test"));
|
||||
|
||||
await Fixture.InitializeAsync();
|
||||
await Fixture.Client.GetAsync("/__signin", CT.None);
|
||||
await Fixture.Client.GetAsync("/__signin", _ct);
|
||||
|
||||
// Act
|
||||
var urlEncoded = await EncodeRequest(Build.AuthNRequestXml(), CT.None);
|
||||
var result = await Fixture.Client.GetAsync($"/saml/signin?SAMLRequest={urlEncoded}", CT.None);
|
||||
var urlEncoded = await EncodeRequest(Build.AuthNRequestXml(), _ct);
|
||||
var result = await Fixture.Client.GetAsync($"/saml/signin?SAMLRequest={urlEncoded}", _ct);
|
||||
|
||||
// Assert - Verify encrypted structure
|
||||
result.StatusCode.ShouldBe(HttpStatusCode.OK);
|
||||
|
||||
var responseData = await ExtractSamlResponse(result, CT.None);
|
||||
var responseData = await ExtractSamlResponse(result, _ct);
|
||||
var responseXml = responseData.responseXml;
|
||||
|
||||
// Verify encryption happened
|
||||
|
|
@ -234,16 +236,16 @@ public class SamlEncryptionTests
|
|||
"Test"));
|
||||
|
||||
await Fixture.InitializeAsync();
|
||||
await Fixture.Client.GetAsync("/__signin", CT.None);
|
||||
await Fixture.Client.GetAsync("/__signin", _ct);
|
||||
|
||||
// Act
|
||||
var urlEncoded = await EncodeRequest(Build.AuthNRequestXml(), CT.None);
|
||||
var result = await Fixture.Client.GetAsync($"/saml/signin?SAMLRequest={urlEncoded}", CT.None);
|
||||
var urlEncoded = await EncodeRequest(Build.AuthNRequestXml(), _ct);
|
||||
var result = await Fixture.Client.GetAsync($"/saml/signin?SAMLRequest={urlEncoded}", _ct);
|
||||
|
||||
// Assert - Verify structure is valid (can't test decryption due to helper limitations)
|
||||
result.StatusCode.ShouldBe(HttpStatusCode.OK);
|
||||
|
||||
var responseData = await ExtractSamlResponse(result, CT.None);
|
||||
var responseData = await ExtractSamlResponse(result, _ct);
|
||||
var responseXml = responseData.responseXml;
|
||||
var (_, _, responseElement) = ParseSamlResponseXml(responseXml);
|
||||
|
||||
|
|
@ -268,16 +270,16 @@ public class SamlEncryptionTests
|
|||
"Test"));
|
||||
|
||||
await Fixture.InitializeAsync();
|
||||
await Fixture.Client.GetAsync("/__signin", CT.None);
|
||||
await Fixture.Client.GetAsync("/__signin", _ct);
|
||||
|
||||
// Act
|
||||
var urlEncoded = await EncodeRequest(Build.AuthNRequestXml(), CT.None);
|
||||
var result = await Fixture.Client.GetAsync($"/saml/signin?SAMLRequest={urlEncoded}", CT.None);
|
||||
var urlEncoded = await EncodeRequest(Build.AuthNRequestXml(), _ct);
|
||||
var result = await Fixture.Client.GetAsync($"/saml/signin?SAMLRequest={urlEncoded}", _ct);
|
||||
|
||||
// Assert
|
||||
result.StatusCode.ShouldBe(HttpStatusCode.OK);
|
||||
|
||||
var responseData = await ExtractSamlResponse(result, CT.None);
|
||||
var responseData = await ExtractSamlResponse(result, _ct);
|
||||
var responseXml = responseData.responseXml;
|
||||
var (_, _, responseElement) = ParseSamlResponseXml(responseXml);
|
||||
|
||||
|
|
@ -307,16 +309,16 @@ public class SamlEncryptionTests
|
|||
"Test"));
|
||||
|
||||
await Fixture.InitializeAsync();
|
||||
await Fixture.Client.GetAsync("/__signin", CT.None);
|
||||
await Fixture.Client.GetAsync("/__signin", _ct);
|
||||
|
||||
// Act
|
||||
var urlEncoded = await EncodeRequest(Build.AuthNRequestXml(), CT.None);
|
||||
var result = await Fixture.Client.GetAsync($"/saml/signin?SAMLRequest={urlEncoded}", CT.None);
|
||||
var urlEncoded = await EncodeRequest(Build.AuthNRequestXml(), _ct);
|
||||
var result = await Fixture.Client.GetAsync($"/saml/signin?SAMLRequest={urlEncoded}", _ct);
|
||||
|
||||
// Assert - Encryption should happen after signing
|
||||
result.StatusCode.ShouldBe(HttpStatusCode.OK);
|
||||
|
||||
var responseData = await ExtractSamlResponse(result, CT.None);
|
||||
var responseData = await ExtractSamlResponse(result, _ct);
|
||||
HasEncryptedAssertion(responseData.responseXml).ShouldBeTrue();
|
||||
}
|
||||
|
||||
|
|
@ -337,16 +339,16 @@ public class SamlEncryptionTests
|
|||
"Test"));
|
||||
|
||||
await Fixture.InitializeAsync();
|
||||
await Fixture.Client.GetAsync("/__signin", CT.None);
|
||||
await Fixture.Client.GetAsync("/__signin", _ct);
|
||||
|
||||
// Act
|
||||
var urlEncoded = await EncodeRequest(Build.AuthNRequestXml(), CT.None);
|
||||
var result = await Fixture.Client.GetAsync($"/saml/signin?SAMLRequest={urlEncoded}", CT.None);
|
||||
var urlEncoded = await EncodeRequest(Build.AuthNRequestXml(), _ct);
|
||||
var result = await Fixture.Client.GetAsync($"/saml/signin?SAMLRequest={urlEncoded}", _ct);
|
||||
|
||||
// Assert
|
||||
result.StatusCode.ShouldBe(HttpStatusCode.OK);
|
||||
|
||||
var responseData = await ExtractSamlResponse(result, CT.None);
|
||||
var responseData = await ExtractSamlResponse(result, _ct);
|
||||
var responseXml = responseData.responseXml;
|
||||
var (_, _, responseElement) = ParseSamlResponseXml(responseXml);
|
||||
|
||||
|
|
@ -376,15 +378,15 @@ public class SamlEncryptionTests
|
|||
"Test"));
|
||||
|
||||
await Fixture.InitializeAsync();
|
||||
await Fixture.Client.GetAsync("/__signin", CT.None);
|
||||
await Fixture.Client.GetAsync("/__signin", _ct);
|
||||
|
||||
// Act
|
||||
var urlEncoded = await EncodeRequest(Build.AuthNRequestXml(), CT.None);
|
||||
var result = await Fixture.Client.GetAsync($"/saml/signin?SAMLRequest={urlEncoded}", CT.None);
|
||||
var urlEncoded = await EncodeRequest(Build.AuthNRequestXml(), _ct);
|
||||
var result = await Fixture.Client.GetAsync($"/saml/signin?SAMLRequest={urlEncoded}", _ct);
|
||||
|
||||
// Assert - Should encrypt successfully with first valid cert
|
||||
result.StatusCode.ShouldBe(HttpStatusCode.OK);
|
||||
var responseData = await ExtractSamlResponse(result, CT.None);
|
||||
var responseData = await ExtractSamlResponse(result, _ct);
|
||||
HasEncryptedAssertion(responseData.responseXml).ShouldBeTrue();
|
||||
}
|
||||
|
||||
|
|
@ -413,11 +415,11 @@ public class SamlEncryptionTests
|
|||
"Test"));
|
||||
|
||||
await Fixture.InitializeAsync();
|
||||
await Fixture.Client.GetAsync("/__signin", CT.None);
|
||||
await Fixture.Client.GetAsync("/__signin", _ct);
|
||||
|
||||
// Act
|
||||
var urlEncoded = await EncodeRequest(Build.AuthNRequestXml(), CT.None);
|
||||
var result = await Fixture.Client.GetAsync($"/saml/signin?SAMLRequest={urlEncoded}", CT.None);
|
||||
var urlEncoded = await EncodeRequest(Build.AuthNRequestXml(), _ct);
|
||||
var result = await Fixture.Client.GetAsync($"/saml/signin?SAMLRequest={urlEncoded}", _ct);
|
||||
|
||||
// Assert - Expired cert is a configuration error, so expect 500
|
||||
result.StatusCode.ShouldBe(HttpStatusCode.InternalServerError);
|
||||
|
|
@ -436,23 +438,23 @@ public class SamlEncryptionTests
|
|||
"Test"));
|
||||
|
||||
await Fixture.InitializeAsync();
|
||||
await Fixture.Client.GetAsync("/__signin", CT.None);
|
||||
await Fixture.Client.GetAsync("/__signin", _ct);
|
||||
|
||||
// Act
|
||||
var urlEncoded = await EncodeRequest(Build.AuthNRequestXml(), CT.None);
|
||||
var result = await Fixture.Client.GetAsync($"/saml/signin?SAMLRequest={urlEncoded}", CT.None);
|
||||
var urlEncoded = await EncodeRequest(Build.AuthNRequestXml(), _ct);
|
||||
var result = await Fixture.Client.GetAsync($"/saml/signin?SAMLRequest={urlEncoded}", _ct);
|
||||
|
||||
// Assert - Should return plain assertion
|
||||
result.StatusCode.ShouldBe(HttpStatusCode.OK);
|
||||
|
||||
var responseData = await ExtractSamlResponse(result, CT.None);
|
||||
var responseData = await ExtractSamlResponse(result, _ct);
|
||||
var responseXml = responseData.responseXml;
|
||||
|
||||
HasPlainAssertion(responseXml).ShouldBeTrue("Response should contain plain Assertion");
|
||||
HasEncryptedAssertion(responseXml).ShouldBeFalse("Response should not be encrypted");
|
||||
|
||||
// Verify can parse as success
|
||||
var samlResponse = await ExtractSamlSuccessFromPostAsync(result, CT.None);
|
||||
var samlResponse = await ExtractSamlSuccessFromPostAsync(result, _ct);
|
||||
samlResponse.StatusCode.ShouldBe(SamlStatusCodes.Success);
|
||||
samlResponse.Assertion.ShouldNotBeNull();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ namespace Duende.IdentityServer.IntegrationTests.Endpoints.Saml;
|
|||
|
||||
internal class SamlFixture : IAsyncLifetime
|
||||
{
|
||||
private readonly Ct _ct = TestContext.Current.CancellationToken;
|
||||
|
||||
public SamlData Data = new SamlData();
|
||||
public SamlDataBuilder Builder => new SamlDataBuilder(Data);
|
||||
|
||||
|
|
@ -247,7 +249,7 @@ internal class SamlFixture : IAsyncLifetime
|
|||
{
|
||||
var samlInteractionService = ctx.RequestServices.GetRequiredService<ISamlInteractionService>();
|
||||
var authenticationRequest =
|
||||
await samlInteractionService.GetAuthenticationRequestContextAsync(CT.None);
|
||||
await samlInteractionService.GetAuthenticationRequestContextAsync(_ct);
|
||||
|
||||
if (authenticationRequest == null)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ public class SamlIdpInitiatedEndpointTests
|
|||
{
|
||||
private const string Category = "SAML IdP-Initiated Endpoint";
|
||||
|
||||
private readonly Ct _ct = TestContext.Current.CancellationToken;
|
||||
|
||||
private SamlFixture Fixture = new();
|
||||
|
||||
private SamlData Data => Fixture.Data;
|
||||
|
|
@ -38,11 +40,11 @@ public class SamlIdpInitiatedEndpointTests
|
|||
|
||||
Fixture.UserToSignIn =
|
||||
new ClaimsPrincipal(new ClaimsIdentity([new Claim(JwtClaimTypes.Subject, "user-id")], "Test"));
|
||||
await Fixture.NonRedirectingClient.GetAsync("/__signin", CT.None);
|
||||
await Fixture.NonRedirectingClient.GetAsync("/__signin", _ct);
|
||||
|
||||
// Act
|
||||
var spEntityId = HttpUtility.UrlEncode(Data.EntityId.ToString());
|
||||
var result = await Fixture.NonRedirectingClient.GetAsync($"/saml/idp-initiated?spEntityId={spEntityId}", CT.None);
|
||||
var result = await Fixture.NonRedirectingClient.GetAsync($"/saml/idp-initiated?spEntityId={spEntityId}", _ct);
|
||||
|
||||
// Assert
|
||||
result.StatusCode.ShouldBe(HttpStatusCode.Found);
|
||||
|
|
@ -67,23 +69,23 @@ public class SamlIdpInitiatedEndpointTests
|
|||
|
||||
Fixture.UserToSignIn =
|
||||
new ClaimsPrincipal(new ClaimsIdentity([new Claim(JwtClaimTypes.Subject, "user-id")], "Test"));
|
||||
await Fixture.NonRedirectingClient.GetAsync("/__signin", CT.None);
|
||||
await Fixture.NonRedirectingClient.GetAsync("/__signin", _ct);
|
||||
|
||||
// Act
|
||||
var spEntityId = HttpUtility.UrlEncode(Data.EntityId.ToString());
|
||||
var relayState = HttpUtility.UrlEncode("/my-app/dashboard");
|
||||
var result = await Fixture.NonRedirectingClient.GetAsync(
|
||||
$"/saml/idp-initiated?spEntityId={spEntityId}&relayState={relayState}", CT.None);
|
||||
$"/saml/idp-initiated?spEntityId={spEntityId}&relayState={relayState}", _ct);
|
||||
|
||||
result.StatusCode.ShouldBe(HttpStatusCode.Found);
|
||||
var redirectUri = result.Headers.Location;
|
||||
redirectUri.ShouldNotBeNull();
|
||||
redirectUri.ToString().ShouldBe($"{SamlConstants.Urls.SamlRoute}{SamlConstants.Urls.SigninCallback}");
|
||||
|
||||
var acsResult = await Fixture.NonRedirectingClient.GetAsync(redirectUri, CT.None);
|
||||
var acsResult = await Fixture.NonRedirectingClient.GetAsync(redirectUri, _ct);
|
||||
|
||||
// Assert
|
||||
var samlResponse = await ExtractSamlSuccessFromPostAsync(acsResult, CT.None);
|
||||
var samlResponse = await ExtractSamlSuccessFromPostAsync(acsResult, _ct);
|
||||
samlResponse.RelayState.ShouldBe(HttpUtility.UrlDecode(relayState));
|
||||
}
|
||||
|
||||
|
|
@ -103,7 +105,7 @@ public class SamlIdpInitiatedEndpointTests
|
|||
|
||||
// Act
|
||||
var spEntityId = HttpUtility.UrlEncode(Data.EntityId.ToString());
|
||||
var result = await Fixture.NonRedirectingClient.GetAsync($"/saml/idp-initiated?spEntityId={spEntityId}", CT.None);
|
||||
var result = await Fixture.NonRedirectingClient.GetAsync($"/saml/idp-initiated?spEntityId={spEntityId}", _ct);
|
||||
|
||||
// Assert
|
||||
result.StatusCode.ShouldBe(HttpStatusCode.Found);
|
||||
|
|
@ -126,11 +128,11 @@ public class SamlIdpInitiatedEndpointTests
|
|||
|
||||
// Act
|
||||
var unknownEntityId = HttpUtility.UrlEncode("https://unknown.example.com");
|
||||
var result = await Fixture.Client.GetAsync($"/saml/idp-initiated?spEntityId={unknownEntityId}", CT.None);
|
||||
var result = await Fixture.Client.GetAsync($"/saml/idp-initiated?spEntityId={unknownEntityId}", _ct);
|
||||
|
||||
// Assert
|
||||
result.StatusCode.ShouldBe(HttpStatusCode.BadRequest);
|
||||
var problemDetails = await result.Content.ReadFromJsonAsync<ProblemDetails>(CT.None);
|
||||
var problemDetails = await result.Content.ReadFromJsonAsync<ProblemDetails>(_ct);
|
||||
problemDetails.ShouldNotBeNull();
|
||||
problemDetails.Detail.ShouldBe("Service Provider 'https://unknown.example.com' is not registered");
|
||||
}
|
||||
|
|
@ -154,11 +156,11 @@ public class SamlIdpInitiatedEndpointTests
|
|||
|
||||
// Act
|
||||
var spEntityId = HttpUtility.UrlEncode(Data.EntityId.ToString());
|
||||
var result = await Fixture.Client.GetAsync($"/saml/idp-initiated?spEntityId={spEntityId}", CT.None);
|
||||
var result = await Fixture.Client.GetAsync($"/saml/idp-initiated?spEntityId={spEntityId}", _ct);
|
||||
|
||||
// Assert
|
||||
result.StatusCode.ShouldBe(HttpStatusCode.BadRequest);
|
||||
var problemDetails = await result.Content.ReadFromJsonAsync<ProblemDetails>(CT.None);
|
||||
var problemDetails = await result.Content.ReadFromJsonAsync<ProblemDetails>(_ct);
|
||||
problemDetails.ShouldNotBeNull();
|
||||
// Disabled SPs are filtered by the store, so they appear as not registered
|
||||
problemDetails.Detail.ShouldBe($"Service Provider '{Data.EntityId}' is not registered");
|
||||
|
|
@ -180,11 +182,11 @@ public class SamlIdpInitiatedEndpointTests
|
|||
|
||||
// Act
|
||||
var spEntityId = HttpUtility.UrlEncode(Data.EntityId.ToString());
|
||||
var result = await Fixture.Client.GetAsync($"/saml/idp-initiated?spEntityId={spEntityId}", CT.None);
|
||||
var result = await Fixture.Client.GetAsync($"/saml/idp-initiated?spEntityId={spEntityId}", _ct);
|
||||
|
||||
// Assert
|
||||
result.StatusCode.ShouldBe(HttpStatusCode.BadRequest);
|
||||
var problemDetails = await result.Content.ReadFromJsonAsync<ProblemDetails>(CT.None);
|
||||
var problemDetails = await result.Content.ReadFromJsonAsync<ProblemDetails>(_ct);
|
||||
problemDetails.ShouldNotBeNull();
|
||||
problemDetails.Detail.ShouldBe($"Service Provider '{Data.EntityId}' does not allow IdP-initiated SSO");
|
||||
}
|
||||
|
|
@ -212,11 +214,11 @@ public class SamlIdpInitiatedEndpointTests
|
|||
var spEntityId = HttpUtility.UrlEncode(Data.EntityId.ToString());
|
||||
var longRelayState = HttpUtility.UrlEncode(new string('a', 100));
|
||||
var result = await Fixture.Client.GetAsync(
|
||||
$"/saml/idp-initiated?spEntityId={spEntityId}&relayState={longRelayState}", CT.None);
|
||||
$"/saml/idp-initiated?spEntityId={spEntityId}&relayState={longRelayState}", _ct);
|
||||
|
||||
// Assert
|
||||
result.StatusCode.ShouldBe(HttpStatusCode.BadRequest);
|
||||
var problemDetails = await result.Content.ReadFromJsonAsync<ProblemDetails>(CT.None);
|
||||
var problemDetails = await result.Content.ReadFromJsonAsync<ProblemDetails>(_ct);
|
||||
problemDetails.ShouldNotBeNull();
|
||||
problemDetails.Detail.ShouldBe("RelayState exceeds maximum length of 50 bytes");
|
||||
}
|
||||
|
|
@ -238,11 +240,11 @@ public class SamlIdpInitiatedEndpointTests
|
|||
|
||||
// Act
|
||||
var spEntityId = HttpUtility.UrlEncode(Data.EntityId.ToString());
|
||||
var result = await Fixture.Client.GetAsync($"/saml/idp-initiated?spEntityId={spEntityId}", CT.None);
|
||||
var result = await Fixture.Client.GetAsync($"/saml/idp-initiated?spEntityId={spEntityId}", _ct);
|
||||
|
||||
// Assert
|
||||
result.StatusCode.ShouldBe(HttpStatusCode.BadRequest);
|
||||
var problemDetails = await result.Content.ReadFromJsonAsync<ProblemDetails>(CT.None);
|
||||
var problemDetails = await result.Content.ReadFromJsonAsync<ProblemDetails>(_ct);
|
||||
problemDetails.ShouldNotBeNull();
|
||||
problemDetails.Detail.ShouldBe($"Service Provider '{Data.EntityId}' has no AssertionConsumerServiceUrls configured");
|
||||
}
|
||||
|
|
@ -267,21 +269,21 @@ public class SamlIdpInitiatedEndpointTests
|
|||
|
||||
Fixture.UserToSignIn =
|
||||
new ClaimsPrincipal(new ClaimsIdentity([new Claim(JwtClaimTypes.Subject, "user-id")], "Test"));
|
||||
await Fixture.NonRedirectingClient.GetAsync("/__signin", CT.None);
|
||||
await Fixture.NonRedirectingClient.GetAsync("/__signin", _ct);
|
||||
|
||||
// Act
|
||||
var spEntityId = HttpUtility.UrlEncode(Data.EntityId.ToString());
|
||||
var result = await Fixture.NonRedirectingClient.GetAsync($"/saml/idp-initiated?spEntityId={spEntityId}", CT.None);
|
||||
var result = await Fixture.NonRedirectingClient.GetAsync($"/saml/idp-initiated?spEntityId={spEntityId}", _ct);
|
||||
|
||||
result.StatusCode.ShouldBe(HttpStatusCode.Found);
|
||||
var redirectUri = result.Headers.Location;
|
||||
redirectUri.ShouldNotBeNull();
|
||||
redirectUri.ToString().ShouldBe($"{SamlConstants.Urls.SamlRoute}{SamlConstants.Urls.SigninCallback}");
|
||||
|
||||
var acsResult = await Fixture.NonRedirectingClient.GetAsync(redirectUri.ToString(), CT.None);
|
||||
var acsResult = await Fixture.NonRedirectingClient.GetAsync(redirectUri.ToString(), _ct);
|
||||
|
||||
// Assert
|
||||
var samlResponse = await ExtractSamlSuccessFromPostAsync(acsResult, CT.None);
|
||||
var samlResponse = await ExtractSamlSuccessFromPostAsync(acsResult, _ct);
|
||||
samlResponse.Destination.ShouldBe(firstAcsUrl.ToString());
|
||||
}
|
||||
|
||||
|
|
@ -306,7 +308,7 @@ public class SamlIdpInitiatedEndpointTests
|
|||
new Claim(JwtClaimTypes.Email, "user@example.com"),
|
||||
new Claim(JwtClaimTypes.Name, "Test User")
|
||||
], "Test"));
|
||||
await Fixture.NonRedirectingClient.GetAsync("/__signin", CT.None);
|
||||
await Fixture.NonRedirectingClient.GetAsync("/__signin", _ct);
|
||||
|
||||
var spEntityId = HttpUtility.UrlEncode(Data.EntityId.ToString());
|
||||
var relayState = HttpUtility.UrlEncode("/target/page");
|
||||
|
|
@ -314,7 +316,7 @@ public class SamlIdpInitiatedEndpointTests
|
|||
// Act
|
||||
// Step 1: Initiate IdP SSO
|
||||
var initiateResult = await Fixture.NonRedirectingClient.GetAsync(
|
||||
$"/saml/idp-initiated?spEntityId={spEntityId}&relayState={relayState}", CT.None);
|
||||
$"/saml/idp-initiated?spEntityId={spEntityId}&relayState={relayState}", _ct);
|
||||
|
||||
initiateResult.StatusCode.ShouldBe(HttpStatusCode.Found);
|
||||
initiateResult.Headers.Location.ShouldNotBeNull();
|
||||
|
|
@ -324,12 +326,12 @@ public class SamlIdpInitiatedEndpointTests
|
|||
stateId.ShouldNotBeNull();
|
||||
|
||||
// Step 2: Follow redirect to signin callback
|
||||
var callbackResult = await Fixture.NonRedirectingClient.GetAsync("/saml/signin_callback", CT.None);
|
||||
var callbackResult = await Fixture.NonRedirectingClient.GetAsync("/saml/signin_callback", _ct);
|
||||
|
||||
// Assert
|
||||
callbackResult.StatusCode.ShouldBe(HttpStatusCode.OK);
|
||||
|
||||
var samlResponse = await ExtractSamlSuccessFromPostAsync(callbackResult, CT.None);
|
||||
var samlResponse = await ExtractSamlSuccessFromPostAsync(callbackResult, _ct);
|
||||
|
||||
samlResponse.ShouldNotBeNull();
|
||||
samlResponse.Issuer.ShouldBe(Fixture.Url());
|
||||
|
|
@ -361,7 +363,7 @@ public class SamlIdpInitiatedEndpointTests
|
|||
|
||||
// Act
|
||||
var spEntityId = HttpUtility.UrlEncode(Data.EntityId.ToString());
|
||||
var result = await Fixture.Client.GetAsync($"/saml/idp-initiated?spEntityId={spEntityId}", CT.None);
|
||||
var result = await Fixture.Client.GetAsync($"/saml/idp-initiated?spEntityId={spEntityId}", _ct);
|
||||
|
||||
// Assert
|
||||
result.StatusCode.ShouldBe(HttpStatusCode.NotFound);
|
||||
|
|
@ -378,7 +380,7 @@ public class SamlIdpInitiatedEndpointTests
|
|||
};
|
||||
await Fixture.InitializeAsync();
|
||||
|
||||
var result = await Fixture.Client.GetAsync("/saml/idp-initiated", CT.None);
|
||||
var result = await Fixture.Client.GetAsync("/saml/idp-initiated", _ct);
|
||||
|
||||
result.StatusCode.ShouldBe(HttpStatusCode.BadRequest);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ public class SamlMetadataEndpointTests
|
|||
{
|
||||
private const string Category = "SAML Metadata Endpoint";
|
||||
|
||||
private readonly Ct _ct = TestContext.Current.CancellationToken;
|
||||
|
||||
private SamlFixture Fixture = new();
|
||||
|
||||
[Fact]
|
||||
|
|
@ -19,14 +21,14 @@ public class SamlMetadataEndpointTests
|
|||
{
|
||||
await Fixture.InitializeAsync();
|
||||
|
||||
var result = await Fixture.Client.GetAsync("/saml/metadata", CT.None);
|
||||
var result = await Fixture.Client.GetAsync("/saml/metadata", _ct);
|
||||
result.StatusCode.ShouldBe(HttpStatusCode.OK);
|
||||
result.Content.Headers.ContentType
|
||||
.ShouldNotBeNull()
|
||||
.MediaType
|
||||
.ShouldBe(SamlConstants.ContentTypes.Metadata);
|
||||
|
||||
var content = await result.Content.ReadAsStringAsync(CT.None);
|
||||
var content = await result.Content.ReadAsStringAsync(_ct);
|
||||
|
||||
var settings = new VerifySettings();
|
||||
var hostUri = Fixture.Url();
|
||||
|
|
@ -49,10 +51,10 @@ public class SamlMetadataEndpointTests
|
|||
|
||||
await Fixture.InitializeAsync();
|
||||
|
||||
var result = await Fixture.Client.GetAsync("/saml/metadata", CT.None);
|
||||
var result = await Fixture.Client.GetAsync("/saml/metadata", _ct);
|
||||
result.StatusCode.ShouldBe(HttpStatusCode.OK);
|
||||
|
||||
var content = await result.Content.ReadAsStringAsync(CT.None);
|
||||
var content = await result.Content.ReadAsStringAsync(_ct);
|
||||
|
||||
var expectedValidUntil = Fixture.Now.Add(TimeSpan.FromDays(30)).UtcDateTime.ToString("yyyy-MM-ddTHH:mm:ssZ");
|
||||
content.ShouldContain($"validUntil=\"{expectedValidUntil}\"");
|
||||
|
|
@ -69,10 +71,10 @@ public class SamlMetadataEndpointTests
|
|||
|
||||
await Fixture.InitializeAsync();
|
||||
|
||||
var result = await Fixture.Client.GetAsync("/saml/metadata", CT.None);
|
||||
var result = await Fixture.Client.GetAsync("/saml/metadata", _ct);
|
||||
result.StatusCode.ShouldBe(HttpStatusCode.OK);
|
||||
|
||||
var content = await result.Content.ReadAsStringAsync(CT.None);
|
||||
var content = await result.Content.ReadAsStringAsync(_ct);
|
||||
|
||||
content.ShouldContain("WantAuthnRequestsSigned=\"true\"");
|
||||
}
|
||||
|
|
@ -90,10 +92,10 @@ public class SamlMetadataEndpointTests
|
|||
|
||||
await Fixture.InitializeAsync();
|
||||
|
||||
var result = await Fixture.Client.GetAsync("/saml/metadata", CT.None);
|
||||
var result = await Fixture.Client.GetAsync("/saml/metadata", _ct);
|
||||
result.StatusCode.ShouldBe(HttpStatusCode.OK);
|
||||
|
||||
var content = await result.Content.ReadAsStringAsync(CT.None);
|
||||
var content = await result.Content.ReadAsStringAsync(_ct);
|
||||
|
||||
content.ShouldContain("<NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</NameIDFormat>");
|
||||
content.ShouldContain("<NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:persistent</NameIDFormat>");
|
||||
|
|
@ -106,10 +108,10 @@ public class SamlMetadataEndpointTests
|
|||
{
|
||||
await Fixture.InitializeAsync();
|
||||
|
||||
var result = await Fixture.Client.GetAsync("/saml/metadata", CT.None);
|
||||
var result = await Fixture.Client.GetAsync("/saml/metadata", _ct);
|
||||
result.StatusCode.ShouldBe(HttpStatusCode.OK);
|
||||
|
||||
var content = await result.Content.ReadAsStringAsync(CT.None);
|
||||
var content = await result.Content.ReadAsStringAsync(_ct);
|
||||
|
||||
content.ShouldContain("<SingleLogoutService");
|
||||
content.ShouldContain("Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\"");
|
||||
|
|
@ -123,10 +125,10 @@ public class SamlMetadataEndpointTests
|
|||
{
|
||||
await Fixture.InitializeAsync();
|
||||
|
||||
var result = await Fixture.Client.GetAsync("/saml/metadata", CT.None);
|
||||
var result = await Fixture.Client.GetAsync("/saml/metadata", _ct);
|
||||
result.StatusCode.ShouldBe(HttpStatusCode.OK);
|
||||
|
||||
var content = await result.Content.ReadAsStringAsync(CT.None);
|
||||
var content = await result.Content.ReadAsStringAsync(_ct);
|
||||
var locationUrls = GetServiceLocationUrls(content, "SingleSignOnService", "SingleLogoutService");
|
||||
|
||||
foreach (var location in locationUrls)
|
||||
|
|
@ -141,10 +143,10 @@ public class SamlMetadataEndpointTests
|
|||
{
|
||||
await Fixture.InitializeAsync();
|
||||
|
||||
var result = await Fixture.Client.GetAsync("/saml/metadata", CT.None);
|
||||
var result = await Fixture.Client.GetAsync("/saml/metadata", _ct);
|
||||
result.StatusCode.ShouldBe(HttpStatusCode.OK);
|
||||
|
||||
var content = await result.Content.ReadAsStringAsync(CT.None);
|
||||
var content = await result.Content.ReadAsStringAsync(_ct);
|
||||
var locationUrls = GetServiceLocationUrls(content, "SingleSignOnService", "SingleLogoutService");
|
||||
|
||||
foreach (var location in locationUrls)
|
||||
|
|
@ -166,10 +168,10 @@ public class SamlMetadataEndpointTests
|
|||
|
||||
await Fixture.InitializeAsync();
|
||||
|
||||
var result = await Fixture.Client.GetAsync("/saml/metadata", CT.None);
|
||||
var result = await Fixture.Client.GetAsync("/saml/metadata", _ct);
|
||||
result.StatusCode.ShouldBe(HttpStatusCode.OK);
|
||||
|
||||
var content = await result.Content.ReadAsStringAsync(CT.None);
|
||||
var content = await result.Content.ReadAsStringAsync(_ct);
|
||||
|
||||
// Should not have double slashes
|
||||
content.ShouldNotContain("saml//signin");
|
||||
|
|
@ -192,10 +194,10 @@ public class SamlMetadataEndpointTests
|
|||
// at the unit level in BuildServiceUrl unit tests
|
||||
await Fixture.InitializeAsync();
|
||||
|
||||
var result = await Fixture.Client.GetAsync("/saml/metadata", CT.None);
|
||||
var result = await Fixture.Client.GetAsync("/saml/metadata", _ct);
|
||||
result.StatusCode.ShouldBe(HttpStatusCode.OK);
|
||||
|
||||
var content = await result.Content.ReadAsStringAsync(CT.None);
|
||||
var content = await result.Content.ReadAsStringAsync(_ct);
|
||||
var locationUrls = GetServiceLocationUrls(content, "SingleSignOnService", "SingleLogoutService");
|
||||
|
||||
foreach (var location in locationUrls)
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ public class SamlSigninCallbackEndpointTests
|
|||
{
|
||||
private const string Category = "SAML Signin Callback Endpoint";
|
||||
|
||||
private readonly Ct _ct = TestContext.Current.CancellationToken;
|
||||
|
||||
private SamlFixture Fixture = new();
|
||||
|
||||
private SamlData Data => Fixture.Data;
|
||||
|
|
@ -34,11 +36,11 @@ public class SamlSigninCallbackEndpointTests
|
|||
|
||||
Fixture.UserToSignIn =
|
||||
new ClaimsPrincipal(new ClaimsIdentity([new Claim(JwtClaimTypes.Subject, "user-id")], "Test"));
|
||||
await Fixture.Client.GetAsync("/__signin", CT.None);
|
||||
await Fixture.Client.GetAsync("/__signin", _ct);
|
||||
|
||||
var authnRequestXml = Build.AuthNRequestXml();
|
||||
var urlEncoded = await EncodeRequest(authnRequestXml, CT.None);
|
||||
var signinResult = await Fixture.NonRedirectingClient.GetAsync($"/saml/signin?SAMLRequest={urlEncoded}", CT.None);
|
||||
var urlEncoded = await EncodeRequest(authnRequestXml, _ct);
|
||||
var signinResult = await Fixture.NonRedirectingClient.GetAsync($"/saml/signin?SAMLRequest={urlEncoded}", _ct);
|
||||
|
||||
signinResult.StatusCode.ShouldBe(HttpStatusCode.Found);
|
||||
signinResult.Headers.Location.ShouldNotBeNull();
|
||||
|
|
@ -48,12 +50,12 @@ public class SamlSigninCallbackEndpointTests
|
|||
|
||||
// Remove state from store so the next request is sent with a state id that for state which no longer exists
|
||||
var samlSigninStateStore = Fixture.Get<ISamlSigninStateStore>();
|
||||
await samlSigninStateStore.RetrieveSigninRequestStateAsync(new StateId(Guid.Parse(stateId)), CT.None);
|
||||
await samlSigninStateStore.RetrieveSigninRequestStateAsync(new StateId(Guid.Parse(stateId)), _ct);
|
||||
|
||||
var result = await Fixture.NonRedirectingClient.GetAsync("/saml/signin_callback", CT.None);
|
||||
var result = await Fixture.NonRedirectingClient.GetAsync("/saml/signin_callback", _ct);
|
||||
|
||||
result.StatusCode.ShouldBe(HttpStatusCode.BadRequest);
|
||||
var problemDetails = await result.Content.ReadFromJsonAsync<ProblemDetails>(CT.None);
|
||||
var problemDetails = await result.Content.ReadFromJsonAsync<ProblemDetails>(_ct);
|
||||
problemDetails.ShouldNotBeNull();
|
||||
problemDetails.Detail.ShouldBe($"The request {stateId} could not be found.");
|
||||
}
|
||||
|
|
@ -67,14 +69,14 @@ public class SamlSigninCallbackEndpointTests
|
|||
|
||||
Fixture.UserToSignIn =
|
||||
new ClaimsPrincipal(new ClaimsIdentity([new Claim(JwtClaimTypes.Subject, "user-id")], "Test"));
|
||||
await Fixture.Client.GetAsync("/__signin", CT.None);
|
||||
await Fixture.Client.GetAsync("/__signin", _ct);
|
||||
|
||||
// Do not make request to the sign-in endpoint first so no state id is created
|
||||
|
||||
var result = await Fixture.Client.GetAsync("/saml/signin_callback", CT.None);
|
||||
var result = await Fixture.Client.GetAsync("/saml/signin_callback", _ct);
|
||||
|
||||
result.StatusCode.ShouldBe(HttpStatusCode.BadRequest);
|
||||
var problemDetails = await result.Content.ReadFromJsonAsync<ProblemDetails>(CT.None);
|
||||
var problemDetails = await result.Content.ReadFromJsonAsync<ProblemDetails>(_ct);
|
||||
problemDetails.ShouldNotBeNull();
|
||||
problemDetails.Detail.ShouldBe("No state id could be found.");
|
||||
}
|
||||
|
|
@ -88,20 +90,20 @@ public class SamlSigninCallbackEndpointTests
|
|||
|
||||
Fixture.UserToSignIn =
|
||||
new ClaimsPrincipal(new ClaimsIdentity([new Claim(JwtClaimTypes.Subject, "user-id")], "Test"));
|
||||
await Fixture.NonRedirectingClient.GetAsync("/__signin", CT.None);
|
||||
await Fixture.NonRedirectingClient.GetAsync("/__signin", _ct);
|
||||
|
||||
var authnRequestXml = Build.AuthNRequestXml();
|
||||
var urlEncoded = await EncodeRequest(authnRequestXml, CT.None);
|
||||
var signinResult = await Fixture.NonRedirectingClient.GetAsync($"/saml/signin?SAMLRequest={urlEncoded}", CT.None);
|
||||
var urlEncoded = await EncodeRequest(authnRequestXml, _ct);
|
||||
var signinResult = await Fixture.NonRedirectingClient.GetAsync($"/saml/signin?SAMLRequest={urlEncoded}", _ct);
|
||||
|
||||
signinResult.StatusCode.ShouldBe(HttpStatusCode.Found);
|
||||
var redirectUri = signinResult.Headers.Location;
|
||||
redirectUri.ShouldNotBeNull();
|
||||
redirectUri.ToString().ShouldStartWith("/saml/signin_callback");
|
||||
|
||||
await Fixture.NonRedirectingClient.GetAsync("/__signout", CT.None);
|
||||
await Fixture.NonRedirectingClient.GetAsync("/__signout", _ct);
|
||||
|
||||
var result = await Fixture.NonRedirectingClient.GetAsync($"/saml/signin_callback", CT.None);
|
||||
var result = await Fixture.NonRedirectingClient.GetAsync($"/saml/signin_callback", _ct);
|
||||
|
||||
result.StatusCode.ShouldBe(HttpStatusCode.Found);
|
||||
var resultRedirectUri = result.Headers.Location;
|
||||
|
|
@ -119,11 +121,11 @@ public class SamlSigninCallbackEndpointTests
|
|||
|
||||
Fixture.UserToSignIn =
|
||||
new ClaimsPrincipal(new ClaimsIdentity([new Claim(JwtClaimTypes.Subject, "user-id")], "Test"));
|
||||
await Fixture.NonRedirectingClient.GetAsync("/__signin", CT.None);
|
||||
await Fixture.NonRedirectingClient.GetAsync("/__signin", _ct);
|
||||
|
||||
var authnRequestXml = Build.AuthNRequestXml();
|
||||
var urlEncoded = await EncodeRequest(authnRequestXml, CT.None);
|
||||
var signinResult = await Fixture.NonRedirectingClient.GetAsync($"/saml/signin?SAMLRequest={urlEncoded}", CT.None);
|
||||
var urlEncoded = await EncodeRequest(authnRequestXml, _ct);
|
||||
var signinResult = await Fixture.NonRedirectingClient.GetAsync($"/saml/signin?SAMLRequest={urlEncoded}", _ct);
|
||||
|
||||
signinResult.StatusCode.ShouldBe(HttpStatusCode.Found);
|
||||
var redirectUri = signinResult.Headers.Location;
|
||||
|
|
@ -131,10 +133,10 @@ public class SamlSigninCallbackEndpointTests
|
|||
|
||||
Fixture.ClearServiceProvidersAsync();
|
||||
|
||||
var result = await Fixture.NonRedirectingClient.GetAsync("/saml/signin_callback", CT.None);
|
||||
var result = await Fixture.NonRedirectingClient.GetAsync("/saml/signin_callback", _ct);
|
||||
|
||||
result.StatusCode.ShouldBe(HttpStatusCode.BadRequest);
|
||||
var problemDetails = await result.Content.ReadFromJsonAsync<ProblemDetails>(CT.None);
|
||||
var problemDetails = await result.Content.ReadFromJsonAsync<ProblemDetails>(_ct);
|
||||
problemDetails.ShouldNotBeNull();
|
||||
problemDetails.Detail.ShouldBe($"Service Provider '{sp.EntityId}' is not registered or is disabled");
|
||||
}
|
||||
|
|
@ -149,11 +151,11 @@ public class SamlSigninCallbackEndpointTests
|
|||
|
||||
Fixture.UserToSignIn =
|
||||
new ClaimsPrincipal(new ClaimsIdentity([new Claim(JwtClaimTypes.Subject, "user-id")], "Test"));
|
||||
await Fixture.NonRedirectingClient.GetAsync("/__signin", CT.None);
|
||||
await Fixture.NonRedirectingClient.GetAsync("/__signin", _ct);
|
||||
|
||||
var authnRequestXml = Build.AuthNRequestXml();
|
||||
var urlEncoded = await EncodeRequest(authnRequestXml, CT.None);
|
||||
var signinResult = await Fixture.NonRedirectingClient.GetAsync($"/saml/signin?SAMLRequest={urlEncoded}", CT.None);
|
||||
var urlEncoded = await EncodeRequest(authnRequestXml, _ct);
|
||||
var signinResult = await Fixture.NonRedirectingClient.GetAsync($"/saml/signin?SAMLRequest={urlEncoded}", _ct);
|
||||
|
||||
signinResult.StatusCode.ShouldBe(HttpStatusCode.Found);
|
||||
var redirectUri = signinResult.Headers.Location;
|
||||
|
|
@ -163,10 +165,10 @@ public class SamlSigninCallbackEndpointTests
|
|||
// we'll rely on everything being in memory and holding onto a reference to the SP in the store for now
|
||||
sp.Enabled = false;
|
||||
|
||||
var result = await Fixture.NonRedirectingClient.GetAsync("/saml/signin_callback", CT.None);
|
||||
var result = await Fixture.NonRedirectingClient.GetAsync("/saml/signin_callback", _ct);
|
||||
|
||||
result.StatusCode.ShouldBe(HttpStatusCode.BadRequest);
|
||||
var problemDetails = await result.Content.ReadFromJsonAsync<ProblemDetails>(CT.None);
|
||||
var problemDetails = await result.Content.ReadFromJsonAsync<ProblemDetails>(_ct);
|
||||
problemDetails.ShouldNotBeNull();
|
||||
problemDetails.Detail.ShouldBe($"Service Provider '{sp.EntityId}' is not registered or is disabled");
|
||||
}
|
||||
|
|
@ -180,30 +182,30 @@ public class SamlSigninCallbackEndpointTests
|
|||
|
||||
Fixture.UserToSignIn =
|
||||
new ClaimsPrincipal(new ClaimsIdentity([new Claim(JwtClaimTypes.Subject, "user-id")], "Test"));
|
||||
await Fixture.NonRedirectingClient.GetAsync("/__signin", CT.None);
|
||||
await Fixture.NonRedirectingClient.GetAsync("/__signin", _ct);
|
||||
|
||||
var authnRequestXml = Build.AuthNRequestXml();
|
||||
var urlEncoded = await EncodeRequest(authnRequestXml, CT.None);
|
||||
var urlEncoded = await EncodeRequest(authnRequestXml, _ct);
|
||||
|
||||
var signinResult = await Fixture.NonRedirectingClient.GetAsync($"/saml/signin?SAMLRequest={urlEncoded}", CT.None);
|
||||
var signinResult = await Fixture.NonRedirectingClient.GetAsync($"/saml/signin?SAMLRequest={urlEncoded}", _ct);
|
||||
|
||||
signinResult.StatusCode.ShouldBe(HttpStatusCode.Found);
|
||||
var redirectUri = signinResult.Headers.Location;
|
||||
redirectUri.ShouldNotBeNull();
|
||||
|
||||
var firstResult = await Fixture.NonRedirectingClient.GetAsync("/saml/signin_callback", CT.None);
|
||||
var firstResult = await Fixture.NonRedirectingClient.GetAsync("/saml/signin_callback", _ct);
|
||||
|
||||
// First use succeeds
|
||||
firstResult.StatusCode.ShouldBe(HttpStatusCode.OK);
|
||||
var html = await firstResult.Content.ReadAsStringAsync(CT.None);
|
||||
var html = await firstResult.Content.ReadAsStringAsync(_ct);
|
||||
html.ShouldContain("SAMLResponse");
|
||||
|
||||
// Second callback with same stateId (replay attack)
|
||||
var secondResult = await Fixture.NonRedirectingClient.GetAsync("/saml/signin_callback", CT.None);
|
||||
var secondResult = await Fixture.NonRedirectingClient.GetAsync("/saml/signin_callback", _ct);
|
||||
|
||||
// Second use should fail
|
||||
secondResult.StatusCode.ShouldBe(HttpStatusCode.BadRequest);
|
||||
var problemDetails = await secondResult.Content.ReadFromJsonAsync<ProblemDetails>(CT.None);
|
||||
var problemDetails = await secondResult.Content.ReadFromJsonAsync<ProblemDetails>(_ct);
|
||||
problemDetails.ShouldNotBeNull();
|
||||
problemDetails.Detail.ShouldBe("No state id could be found.");
|
||||
}
|
||||
|
|
@ -217,12 +219,12 @@ public class SamlSigninCallbackEndpointTests
|
|||
|
||||
Fixture.UserToSignIn =
|
||||
new ClaimsPrincipal(new ClaimsIdentity([new Claim(JwtClaimTypes.Subject, "user-id")], "Test"));
|
||||
await Fixture.NonRedirectingClient.GetAsync("/__signin", CT.None);
|
||||
await Fixture.NonRedirectingClient.GetAsync("/__signin", _ct);
|
||||
|
||||
var authnRequestXml = Build.AuthNRequestXml();
|
||||
var urlEncoded = await EncodeRequest(authnRequestXml, CT.None);
|
||||
var urlEncoded = await EncodeRequest(authnRequestXml, _ct);
|
||||
|
||||
var signinResult = await Fixture.NonRedirectingClient.GetAsync($"/saml/signin?SAMLRequest={urlEncoded}", CT.None);
|
||||
var signinResult = await Fixture.NonRedirectingClient.GetAsync($"/saml/signin?SAMLRequest={urlEncoded}", _ct);
|
||||
|
||||
signinResult.StatusCode.ShouldBe(HttpStatusCode.Found);
|
||||
signinResult.Headers.Location.ShouldNotBeNull();
|
||||
|
|
@ -231,10 +233,10 @@ public class SamlSigninCallbackEndpointTests
|
|||
|
||||
Fixture.Data.FakeTimeProvider.Advance(TimeSpan.FromMinutes(11));
|
||||
|
||||
var result = await Fixture.NonRedirectingClient.GetAsync("/saml/signin_callback", CT.None);
|
||||
var result = await Fixture.NonRedirectingClient.GetAsync("/saml/signin_callback", _ct);
|
||||
|
||||
result.StatusCode.ShouldBe(HttpStatusCode.BadRequest);
|
||||
var problemDetails = await result.Content.ReadFromJsonAsync<ProblemDetails>(CT.None);
|
||||
var problemDetails = await result.Content.ReadFromJsonAsync<ProblemDetails>(_ct);
|
||||
problemDetails.ShouldNotBeNull();
|
||||
problemDetails.Detail.ShouldBe($"The request {stateId} could not be found.");
|
||||
}
|
||||
|
|
@ -248,22 +250,22 @@ public class SamlSigninCallbackEndpointTests
|
|||
|
||||
Fixture.UserToSignIn =
|
||||
new ClaimsPrincipal(new ClaimsIdentity([new Claim(JwtClaimTypes.Subject, "user-id")], "Test"));
|
||||
await Fixture.NonRedirectingClient.GetAsync("/__signin", CT.None);
|
||||
await Fixture.NonRedirectingClient.GetAsync("/__signin", _ct);
|
||||
|
||||
var specificRelayState = "test-relay-state-123";
|
||||
var authnRequestXml = Build.AuthNRequestXml();
|
||||
var urlEncoded = await EncodeRequest(authnRequestXml, CT.None);
|
||||
var urlEncoded = await EncodeRequest(authnRequestXml, _ct);
|
||||
|
||||
var signinResult = await Fixture.NonRedirectingClient.GetAsync($"/saml/signin?SAMLRequest={urlEncoded}&RelayState={specificRelayState}", CT.None);
|
||||
var signinResult = await Fixture.NonRedirectingClient.GetAsync($"/saml/signin?SAMLRequest={urlEncoded}&RelayState={specificRelayState}", _ct);
|
||||
|
||||
signinResult.StatusCode.ShouldBe(HttpStatusCode.Found);
|
||||
var redirectUri = signinResult.Headers.Location;
|
||||
redirectUri.ShouldNotBeNull();
|
||||
|
||||
var result = await Fixture.NonRedirectingClient.GetAsync("/saml/signin_callback", CT.None);
|
||||
var result = await Fixture.NonRedirectingClient.GetAsync("/saml/signin_callback", _ct);
|
||||
|
||||
result.StatusCode.ShouldBe(HttpStatusCode.OK);
|
||||
var samlResponse = await ExtractSamlSuccessFromPostAsync(result, CT.None);
|
||||
var samlResponse = await ExtractSamlSuccessFromPostAsync(result, _ct);
|
||||
samlResponse.ShouldNotBeNull();
|
||||
samlResponse.RelayState.ShouldBe(specificRelayState);
|
||||
}
|
||||
|
|
@ -279,24 +281,24 @@ public class SamlSigninCallbackEndpointTests
|
|||
|
||||
Fixture.UserToSignIn =
|
||||
new ClaimsPrincipal(new ClaimsIdentity([new Claim(JwtClaimTypes.Subject, "user-id")], "Test"));
|
||||
await Fixture.NonRedirectingClient.GetAsync("/__signin", CT.None);
|
||||
await Fixture.NonRedirectingClient.GetAsync("/__signin", _ct);
|
||||
|
||||
var authnRequestXml = Build.AuthNRequestXml();
|
||||
var urlEncoded = await EncodeRequest(authnRequestXml, CT.None);
|
||||
var signinResult = await Fixture.NonRedirectingClient.GetAsync($"/saml/signin?SAMLRequest={urlEncoded}", CT.None);
|
||||
var urlEncoded = await EncodeRequest(authnRequestXml, _ct);
|
||||
var signinResult = await Fixture.NonRedirectingClient.GetAsync($"/saml/signin?SAMLRequest={urlEncoded}", _ct);
|
||||
|
||||
signinResult.StatusCode.ShouldBe(HttpStatusCode.Found);
|
||||
var redirectUri = signinResult.Headers.Location;
|
||||
redirectUri.ShouldNotBeNull();
|
||||
|
||||
var result = await Fixture.NonRedirectingClient.GetAsync("/saml/signin_callback", CT.None);
|
||||
var result = await Fixture.NonRedirectingClient.GetAsync("/saml/signin_callback", _ct);
|
||||
|
||||
result.StatusCode.ShouldBe(HttpStatusCode.OK);
|
||||
var (responseXml, _, _) = await ExtractSamlResponse(result, CT.None);
|
||||
var (responseXml, _, _) = await ExtractSamlResponse(result, _ct);
|
||||
|
||||
VerifySignaturePresence(responseXml, expectResponseSignature: true, expectAssertionSignature: false);
|
||||
|
||||
var successResponse = await ExtractSamlSuccessFromPostAsync(result, CT.None);
|
||||
var successResponse = await ExtractSamlSuccessFromPostAsync(result, _ct);
|
||||
successResponse.StatusCode.ShouldBe("urn:oasis:names:tc:SAML:2.0:status:Success");
|
||||
successResponse.Assertion.Subject?.NameId.ShouldBe("user-id");
|
||||
}
|
||||
|
|
@ -312,24 +314,24 @@ public class SamlSigninCallbackEndpointTests
|
|||
|
||||
Fixture.UserToSignIn =
|
||||
new ClaimsPrincipal(new ClaimsIdentity([new Claim(JwtClaimTypes.Subject, "user-id")], "Test"));
|
||||
await Fixture.NonRedirectingClient.GetAsync("/__signin", CT.None);
|
||||
await Fixture.NonRedirectingClient.GetAsync("/__signin", _ct);
|
||||
|
||||
var authnRequestXml = Build.AuthNRequestXml();
|
||||
var urlEncoded = await EncodeRequest(authnRequestXml, CT.None);
|
||||
var signinResult = await Fixture.NonRedirectingClient.GetAsync($"/saml/signin?SAMLRequest={urlEncoded}", CT.None);
|
||||
var urlEncoded = await EncodeRequest(authnRequestXml, _ct);
|
||||
var signinResult = await Fixture.NonRedirectingClient.GetAsync($"/saml/signin?SAMLRequest={urlEncoded}", _ct);
|
||||
|
||||
signinResult.StatusCode.ShouldBe(HttpStatusCode.Found);
|
||||
var redirectUri = signinResult.Headers.Location;
|
||||
redirectUri.ShouldNotBeNull();
|
||||
|
||||
var result = await Fixture.NonRedirectingClient.GetAsync("/saml/signin_callback", CT.None);
|
||||
var result = await Fixture.NonRedirectingClient.GetAsync("/saml/signin_callback", _ct);
|
||||
|
||||
result.StatusCode.ShouldBe(HttpStatusCode.OK);
|
||||
var (responseXml, _, _) = await ExtractSamlResponse(result, CT.None);
|
||||
var (responseXml, _, _) = await ExtractSamlResponse(result, _ct);
|
||||
|
||||
VerifySignaturePresence(responseXml, expectResponseSignature: false, expectAssertionSignature: true);
|
||||
|
||||
var successResponse = await ExtractSamlSuccessFromPostAsync(result, CT.None);
|
||||
var successResponse = await ExtractSamlSuccessFromPostAsync(result, _ct);
|
||||
successResponse.StatusCode.ShouldBe("urn:oasis:names:tc:SAML:2.0:status:Success");
|
||||
successResponse.Assertion.Subject?.NameId.ShouldBe("user-id");
|
||||
}
|
||||
|
|
@ -345,24 +347,24 @@ public class SamlSigninCallbackEndpointTests
|
|||
|
||||
Fixture.UserToSignIn =
|
||||
new ClaimsPrincipal(new ClaimsIdentity([new Claim(JwtClaimTypes.Subject, "user-id")], "Test"));
|
||||
await Fixture.NonRedirectingClient.GetAsync("/__signin", CT.None);
|
||||
await Fixture.NonRedirectingClient.GetAsync("/__signin", _ct);
|
||||
|
||||
var authnRequestXml = Build.AuthNRequestXml();
|
||||
var urlEncoded = await EncodeRequest(authnRequestXml, CT.None);
|
||||
var signinResult = await Fixture.NonRedirectingClient.GetAsync($"/saml/signin?SAMLRequest={urlEncoded}", CT.None);
|
||||
var urlEncoded = await EncodeRequest(authnRequestXml, _ct);
|
||||
var signinResult = await Fixture.NonRedirectingClient.GetAsync($"/saml/signin?SAMLRequest={urlEncoded}", _ct);
|
||||
|
||||
signinResult.StatusCode.ShouldBe(HttpStatusCode.Found);
|
||||
var redirectUri = signinResult.Headers.Location;
|
||||
redirectUri.ShouldNotBeNull();
|
||||
|
||||
var result = await Fixture.NonRedirectingClient.GetAsync("/saml/signin_callback", CT.None);
|
||||
var result = await Fixture.NonRedirectingClient.GetAsync("/saml/signin_callback", _ct);
|
||||
|
||||
result.StatusCode.ShouldBe(HttpStatusCode.OK);
|
||||
var (responseXml, _, _) = await ExtractSamlResponse(result, CT.None);
|
||||
var (responseXml, _, _) = await ExtractSamlResponse(result, _ct);
|
||||
|
||||
VerifySignaturePresence(responseXml, expectResponseSignature: false, expectAssertionSignature: false);
|
||||
|
||||
var successResponse = await ExtractSamlSuccessFromPostAsync(result, CT.None);
|
||||
var successResponse = await ExtractSamlSuccessFromPostAsync(result, _ct);
|
||||
successResponse.StatusCode.ShouldBe("urn:oasis:names:tc:SAML:2.0:status:Success");
|
||||
successResponse.Assertion.Subject?.NameId.ShouldBe("user-id");
|
||||
}
|
||||
|
|
@ -404,23 +406,23 @@ public class SamlSigninCallbackEndpointTests
|
|||
};
|
||||
|
||||
Fixture.UserToSignIn = new ClaimsPrincipal(new ClaimsIdentity(claims, "Test"));
|
||||
await Fixture.NonRedirectingClient.GetAsync("/__signin", CT.None);
|
||||
await Fixture.NonRedirectingClient.GetAsync("/__signin", _ct);
|
||||
|
||||
var authnRequestXml = Build.AuthNRequestXml();
|
||||
var urlEncoded = await EncodeRequest(authnRequestXml, CT.None);
|
||||
var signinResult = await Fixture.NonRedirectingClient.GetAsync($"/saml/signin?SAMLRequest={urlEncoded}", CT.None);
|
||||
var urlEncoded = await EncodeRequest(authnRequestXml, _ct);
|
||||
var signinResult = await Fixture.NonRedirectingClient.GetAsync($"/saml/signin?SAMLRequest={urlEncoded}", _ct);
|
||||
|
||||
signinResult.StatusCode.ShouldBe(HttpStatusCode.Found);
|
||||
signinResult.Headers.Location.ShouldNotBeNull();
|
||||
|
||||
var result = await Fixture.NonRedirectingClient.GetAsync("/saml/signin_callback", CT.None);
|
||||
var result = await Fixture.NonRedirectingClient.GetAsync("/saml/signin_callback", _ct);
|
||||
|
||||
result.StatusCode.ShouldBe(HttpStatusCode.OK);
|
||||
var (responseXml, _, _) = await ExtractSamlResponse(result, CT.None);
|
||||
var (responseXml, _, _) = await ExtractSamlResponse(result, _ct);
|
||||
|
||||
VerifySignaturePresence(responseXml, expectResponseSignature: true, expectAssertionSignature: true);
|
||||
|
||||
var successResponse = await ExtractSamlSuccessFromPostAsync(result, CT.None);
|
||||
var successResponse = await ExtractSamlSuccessFromPostAsync(result, _ct);
|
||||
successResponse.StatusCode.ShouldBe("urn:oasis:names:tc:SAML:2.0:status:Success");
|
||||
successResponse.Assertion.Attributes.ShouldNotBeNull();
|
||||
successResponse.Assertion.Attributes!.Count.ShouldBeGreaterThan(4); // At least the claims we added
|
||||
|
|
@ -444,26 +446,26 @@ public class SamlSigninCallbackEndpointTests
|
|||
};
|
||||
|
||||
Fixture.UserToSignIn = new ClaimsPrincipal(new ClaimsIdentity(claims, "Test"));
|
||||
await Fixture.NonRedirectingClient.GetAsync("/__signin", CT.None);
|
||||
await Fixture.NonRedirectingClient.GetAsync("/__signin", _ct);
|
||||
|
||||
var authnRequestXml = Build.AuthNRequestXml();
|
||||
var urlEncoded = await EncodeRequest(authnRequestXml, CT.None);
|
||||
var signinResult = await Fixture.NonRedirectingClient.GetAsync($"/saml/signin?SAMLRequest={urlEncoded}", CT.None);
|
||||
var urlEncoded = await EncodeRequest(authnRequestXml, _ct);
|
||||
var signinResult = await Fixture.NonRedirectingClient.GetAsync($"/saml/signin?SAMLRequest={urlEncoded}", _ct);
|
||||
|
||||
signinResult.StatusCode.ShouldBe(HttpStatusCode.Found);
|
||||
signinResult.Headers.Location.ShouldNotBeNull();
|
||||
|
||||
var result = await Fixture.NonRedirectingClient.GetAsync("/saml/signin_callback", CT.None);
|
||||
var result = await Fixture.NonRedirectingClient.GetAsync("/saml/signin_callback", _ct);
|
||||
|
||||
result.StatusCode.ShouldBe(HttpStatusCode.OK);
|
||||
var (responseXml, _, _) = await ExtractSamlResponse(result, CT.None);
|
||||
var (responseXml, _, _) = await ExtractSamlResponse(result, _ct);
|
||||
|
||||
VerifySignaturePresence(responseXml, expectResponseSignature: false, expectAssertionSignature: true);
|
||||
|
||||
var (_, _, responseElement) = ParseSamlResponseXml(responseXml);
|
||||
responseElement.ShouldNotBeNull();
|
||||
|
||||
var successResponse = await ExtractSamlSuccessFromPostAsync(result, CT.None);
|
||||
var successResponse = await ExtractSamlSuccessFromPostAsync(result, _ct);
|
||||
successResponse.StatusCode.ShouldBe("urn:oasis:names:tc:SAML:2.0:status:Success");
|
||||
|
||||
var nameAttribute = successResponse.Assertion.Attributes?.FirstOrDefault(a => a.Name == "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name");
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ public class SamlSigninEndpointTests
|
|||
{
|
||||
private const string Category = "SAML Signin Endpoint";
|
||||
|
||||
private readonly CT _ct = CT.None;
|
||||
private readonly Ct _ct = TestContext.Current.CancellationToken;
|
||||
|
||||
private SamlFixture Fixture = new();
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ public class SamlSingleLogoutCallbackEndpointTests
|
|||
{
|
||||
private const string Category = "SAML single logout callback endpoint";
|
||||
|
||||
private readonly Ct _ct = TestContext.Current.CancellationToken;
|
||||
|
||||
private SamlFixture Fixture = new();
|
||||
|
||||
private SamlData Data => Fixture.Data;
|
||||
|
|
@ -27,7 +29,7 @@ public class SamlSingleLogoutCallbackEndpointTests
|
|||
await Fixture.InitializeAsync();
|
||||
|
||||
// Act
|
||||
var result = await Fixture.Client.PostAsync("/saml/logout_callback", new StringContent(""), CT.None);
|
||||
var result = await Fixture.Client.PostAsync("/saml/logout_callback", new StringContent(""), _ct);
|
||||
|
||||
// Assert
|
||||
result.StatusCode.ShouldBe(HttpStatusCode.MethodNotAllowed);
|
||||
|
|
@ -42,7 +44,7 @@ public class SamlSingleLogoutCallbackEndpointTests
|
|||
await Fixture.InitializeAsync();
|
||||
|
||||
// Act
|
||||
var result = await Fixture.Client.GetAsync("/saml/logout_callback", CT.None);
|
||||
var result = await Fixture.Client.GetAsync("/saml/logout_callback", _ct);
|
||||
|
||||
// Assert
|
||||
result.StatusCode.ShouldBe(HttpStatusCode.BadRequest);
|
||||
|
|
@ -57,7 +59,7 @@ public class SamlSingleLogoutCallbackEndpointTests
|
|||
await Fixture.InitializeAsync();
|
||||
|
||||
// Act
|
||||
var result = await Fixture.Client.GetAsync("/saml/logout_callback?logoutId=invalid", CT.None);
|
||||
var result = await Fixture.Client.GetAsync("/saml/logout_callback?logoutId=invalid", _ct);
|
||||
|
||||
// Assert
|
||||
result.StatusCode.ShouldBe(HttpStatusCode.BadRequest);
|
||||
|
|
@ -82,13 +84,13 @@ public class SamlSingleLogoutCallbackEndpointTests
|
|||
SamlRelayState = null
|
||||
};
|
||||
var messageStore = Fixture.Get<IMessageStore<LogoutMessage>>();
|
||||
var logoutId = await messageStore.WriteAsync(new Message<LogoutMessage>(logoutMessage, DateTime.UtcNow));
|
||||
var logoutId = await messageStore.WriteAsync(new Message<LogoutMessage>(logoutMessage, DateTime.UtcNow), _ct);
|
||||
|
||||
// Act
|
||||
var result = await Fixture.Client.GetAsync($"/saml/logout_callback?logoutId={logoutId}", CT.None);
|
||||
var result = await Fixture.Client.GetAsync($"/saml/logout_callback?logoutId={logoutId}", _ct);
|
||||
|
||||
// Assert
|
||||
var samlResponse = await SamlTestHelpers.ExtractSamlLogoutResponseFromPostAsync(result, CT.None);
|
||||
var samlResponse = await SamlTestHelpers.ExtractSamlLogoutResponseFromPostAsync(result, _ct);
|
||||
samlResponse.StatusCode.ShouldBe(SamlStatusCodes.Success);
|
||||
}
|
||||
|
||||
|
|
@ -110,13 +112,13 @@ public class SamlSingleLogoutCallbackEndpointTests
|
|||
SamlRelayState = "mystate123"
|
||||
};
|
||||
var messageStore = Fixture.Get<IMessageStore<LogoutMessage>>();
|
||||
var logoutId = await messageStore.WriteAsync(new Message<LogoutMessage>(logoutMessage, DateTime.UtcNow));
|
||||
var logoutId = await messageStore.WriteAsync(new Message<LogoutMessage>(logoutMessage, DateTime.UtcNow), _ct);
|
||||
|
||||
// Act
|
||||
var result = await Fixture.Client.GetAsync($"/saml/logout_callback?logoutId={logoutId}", CT.None);
|
||||
var result = await Fixture.Client.GetAsync($"/saml/logout_callback?logoutId={logoutId}", _ct);
|
||||
|
||||
// Assert
|
||||
var response = await SamlTestHelpers.ExtractSamlLogoutResponseFromPostAsync(result, CT.None);
|
||||
var response = await SamlTestHelpers.ExtractSamlLogoutResponseFromPostAsync(result, _ct);
|
||||
response.RelayState.ShouldBe(logoutMessage.SamlRelayState);
|
||||
}
|
||||
|
||||
|
|
@ -138,10 +140,10 @@ public class SamlSingleLogoutCallbackEndpointTests
|
|||
SamlLogoutRequestId = "_abc123"
|
||||
};
|
||||
var messageStore = Fixture.Get<IMessageStore<LogoutMessage>>();
|
||||
var logoutId = await messageStore.WriteAsync(new Message<LogoutMessage>(logoutMessage, DateTime.UtcNow));
|
||||
var logoutId = await messageStore.WriteAsync(new Message<LogoutMessage>(logoutMessage, DateTime.UtcNow), _ct);
|
||||
|
||||
// Act
|
||||
var result = await Fixture.Client.GetAsync($"/saml/logout_callback?logoutId={logoutId}", CT.None);
|
||||
var result = await Fixture.Client.GetAsync($"/saml/logout_callback?logoutId={logoutId}", _ct);
|
||||
|
||||
// Assert
|
||||
result.StatusCode.ShouldBe(HttpStatusCode.BadRequest);
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ public class SamlSingleLogoutEndpointTests
|
|||
{
|
||||
private const string Category = "SAML single logout endpoint";
|
||||
|
||||
private readonly Ct _ct = TestContext.Current.CancellationToken;
|
||||
|
||||
private SamlFixture Fixture = new();
|
||||
|
||||
private SamlData Data => Fixture.Data;
|
||||
|
|
@ -33,11 +35,11 @@ public class SamlSingleLogoutEndpointTests
|
|||
await Fixture.InitializeAsync();
|
||||
|
||||
// Act
|
||||
var result = await Fixture.Client.GetAsync("/saml/logout", CT.None);
|
||||
var result = await Fixture.Client.GetAsync("/saml/logout", _ct);
|
||||
|
||||
// Assert
|
||||
result.StatusCode.ShouldBe(HttpStatusCode.BadRequest);
|
||||
var problemDetails = await result.Content.ReadFromJsonAsync<ProblemDetails>(CT.None);
|
||||
var problemDetails = await result.Content.ReadFromJsonAsync<ProblemDetails>(_ct);
|
||||
problemDetails.ShouldNotBeNull();
|
||||
problemDetails.Detail.ShouldBe("Missing 'SAMLRequest' query parameter in SAML logout request");
|
||||
}
|
||||
|
|
@ -54,11 +56,11 @@ public class SamlSingleLogoutEndpointTests
|
|||
var stringContent = new StringContent(logoutRequestXml, Encoding.UTF8, "application/xml");
|
||||
|
||||
// Act
|
||||
var result = await Fixture.Client.PostAsync("/saml/logout", stringContent, CT.None);
|
||||
var result = await Fixture.Client.PostAsync("/saml/logout", stringContent, _ct);
|
||||
|
||||
// Assert
|
||||
result.StatusCode.ShouldBe(HttpStatusCode.BadRequest);
|
||||
var problemDetails = await result.Content.ReadFromJsonAsync<ProblemDetails>(CT.None);
|
||||
var problemDetails = await result.Content.ReadFromJsonAsync<ProblemDetails>(_ct);
|
||||
problemDetails.ShouldNotBeNull();
|
||||
problemDetails.Detail.ShouldBe("POST request does not have form content type for SAML logout request");
|
||||
}
|
||||
|
|
@ -72,7 +74,7 @@ public class SamlSingleLogoutEndpointTests
|
|||
await Fixture.InitializeAsync();
|
||||
|
||||
var logoutRequestXml = Build.LogoutRequestXml();
|
||||
var encodedRequest = await EncodeRequest(logoutRequestXml, CT.None);
|
||||
var encodedRequest = await EncodeRequest(logoutRequestXml, _ct);
|
||||
var formData = new Dictionary<string, string>
|
||||
{
|
||||
{ "wrong_form_key", encodedRequest }
|
||||
|
|
@ -80,11 +82,11 @@ public class SamlSingleLogoutEndpointTests
|
|||
var content = new FormUrlEncodedContent(formData);
|
||||
|
||||
// Act
|
||||
var result = await Fixture.Client.PostAsync("/saml/logout", content, CT.None);
|
||||
var result = await Fixture.Client.PostAsync("/saml/logout", content, _ct);
|
||||
|
||||
// Assert
|
||||
result.StatusCode.ShouldBe(HttpStatusCode.BadRequest);
|
||||
var problemDetails = await result.Content.ReadFromJsonAsync<ProblemDetails>(CT.None);
|
||||
var problemDetails = await result.Content.ReadFromJsonAsync<ProblemDetails>(_ct);
|
||||
problemDetails.ShouldNotBeNull();
|
||||
problemDetails.Detail.ShouldBe("Missing 'SAMLRequest' form parameter in SAML logout request");
|
||||
}
|
||||
|
|
@ -98,14 +100,14 @@ public class SamlSingleLogoutEndpointTests
|
|||
|
||||
var issuer = "https://wrong-issuer.com";
|
||||
var logoutRequestXml = Build.LogoutRequestXml(issuer: issuer);
|
||||
var urlEncoded = await EncodeRequest(logoutRequestXml, CT.None);
|
||||
var urlEncoded = await EncodeRequest(logoutRequestXml, _ct);
|
||||
|
||||
// Act
|
||||
var result = await Fixture.Client.GetAsync($"/saml/logout?SAMLRequest={urlEncoded}", CT.None);
|
||||
var result = await Fixture.Client.GetAsync($"/saml/logout?SAMLRequest={urlEncoded}", _ct);
|
||||
|
||||
// Assert
|
||||
result.StatusCode.ShouldBe(HttpStatusCode.BadRequest);
|
||||
var problemDetails = await result.Content.ReadFromJsonAsync<ProblemDetails>(CT.None);
|
||||
var problemDetails = await result.Content.ReadFromJsonAsync<ProblemDetails>(_ct);
|
||||
problemDetails.ShouldNotBeNull();
|
||||
problemDetails.Detail.ShouldBe($"Service Provider '{issuer}' is not registered or is disabled");
|
||||
}
|
||||
|
|
@ -121,14 +123,14 @@ public class SamlSingleLogoutEndpointTests
|
|||
await Fixture.InitializeAsync();
|
||||
|
||||
var logoutRequestXml = Build.LogoutRequestXml(issuer: sp.EntityId);
|
||||
var urlEncoded = await EncodeRequest(logoutRequestXml, CT.None);
|
||||
var urlEncoded = await EncodeRequest(logoutRequestXml, _ct);
|
||||
|
||||
// Act
|
||||
var result = await Fixture.Client.GetAsync($"/saml/logout?SAMLRequest={urlEncoded}", CT.None);
|
||||
var result = await Fixture.Client.GetAsync($"/saml/logout?SAMLRequest={urlEncoded}", _ct);
|
||||
|
||||
// Assert
|
||||
result.StatusCode.ShouldBe(HttpStatusCode.BadRequest);
|
||||
var problemDetails = await result.Content.ReadFromJsonAsync<ProblemDetails>(CT.None);
|
||||
var problemDetails = await result.Content.ReadFromJsonAsync<ProblemDetails>(_ct);
|
||||
problemDetails.ShouldNotBeNull();
|
||||
problemDetails.Detail.ShouldBe($"Service Provider '{sp.EntityId}' is not registered or is disabled");
|
||||
}
|
||||
|
|
@ -147,19 +149,19 @@ public class SamlSingleLogoutEndpointTests
|
|||
// Sign in a user first
|
||||
Fixture.UserToSignIn =
|
||||
new ClaimsPrincipal(new ClaimsIdentity([new Claim(JwtClaimTypes.Subject, "user-id")], "Test"));
|
||||
await Fixture.Client.GetAsync("/__signin", CT.None);
|
||||
await Fixture.Client.GetAsync("/__signin", _ct);
|
||||
|
||||
var logoutRequestXml = Build.LogoutRequestXml(
|
||||
destination: new Uri($"{Fixture.Url()}/saml/logout"),
|
||||
sessionIndex: "session123");
|
||||
var urlEncoded = await EncodeAndSignRequest(logoutRequestXml, sp, CT.None);
|
||||
var urlEncoded = await EncodeAndSignRequest(logoutRequestXml, sp, _ct);
|
||||
|
||||
// Act
|
||||
var result = await Fixture.Client.GetAsync($"/saml/logout?SAMLRequest={urlEncoded}", CT.None);
|
||||
var result = await Fixture.Client.GetAsync($"/saml/logout?SAMLRequest={urlEncoded}", _ct);
|
||||
|
||||
// Assert
|
||||
result.StatusCode.ShouldBe(HttpStatusCode.BadRequest);
|
||||
var problemDetails = await result.Content.ReadFromJsonAsync<ProblemDetails>(CT.None);
|
||||
var problemDetails = await result.Content.ReadFromJsonAsync<ProblemDetails>(_ct);
|
||||
problemDetails.ShouldNotBeNull();
|
||||
problemDetails.Detail.ShouldBe($"Service Provider '{sp.EntityId}' has no SingleLogoutServiceUrl configured");
|
||||
}
|
||||
|
|
@ -177,13 +179,13 @@ public class SamlSingleLogoutEndpointTests
|
|||
var logoutRequestXml = Build.LogoutRequestXml(
|
||||
destination: new Uri($"{Fixture.Url()}/saml/logout"),
|
||||
version: "1.0");
|
||||
var urlEncoded = await EncodeAndSignRequest(logoutRequestXml, sp, CT.None);
|
||||
var urlEncoded = await EncodeAndSignRequest(logoutRequestXml, sp, _ct);
|
||||
|
||||
// Act
|
||||
var result = await Fixture.Client.GetAsync($"/saml/logout?SAMLRequest={urlEncoded}", CT.None);
|
||||
var result = await Fixture.Client.GetAsync($"/saml/logout?SAMLRequest={urlEncoded}", _ct);
|
||||
|
||||
// Assert
|
||||
var logoutResponse = await ExtractSamlLogoutResponseFromPostAsync(result, CT.None);
|
||||
var logoutResponse = await ExtractSamlLogoutResponseFromPostAsync(result, _ct);
|
||||
logoutResponse.StatusCode.ShouldBe(SamlStatusCodes.VersionMismatch);
|
||||
}
|
||||
|
||||
|
|
@ -202,13 +204,13 @@ public class SamlSingleLogoutEndpointTests
|
|||
destination: new Uri($"{Fixture.Url()}/saml/logout"),
|
||||
issueInstant: futureTime,
|
||||
sessionIndex: "session123");
|
||||
var urlEncoded = await EncodeAndSignRequest(logoutRequestXml, sp, CT.None);
|
||||
var urlEncoded = await EncodeAndSignRequest(logoutRequestXml, sp, _ct);
|
||||
|
||||
// Act
|
||||
var result = await Fixture.Client.GetAsync($"/saml/logout?SAMLRequest={urlEncoded}", CT.None);
|
||||
var result = await Fixture.Client.GetAsync($"/saml/logout?SAMLRequest={urlEncoded}", _ct);
|
||||
|
||||
// Assert
|
||||
var logoutResponse = await ExtractSamlLogoutResponseFromPostAsync(result, CT.None);
|
||||
var logoutResponse = await ExtractSamlLogoutResponseFromPostAsync(result, _ct);
|
||||
logoutResponse.StatusCode.ShouldBe(SamlStatusCodes.Requester);
|
||||
logoutResponse.StatusMessage.ShouldBe("Request IssueInstant is in the future");
|
||||
}
|
||||
|
|
@ -228,13 +230,13 @@ public class SamlSingleLogoutEndpointTests
|
|||
destination: new Uri($"{Fixture.Url()}/saml/logout"),
|
||||
issueInstant: oldTime,
|
||||
sessionIndex: "session123");
|
||||
var urlEncoded = await EncodeAndSignRequest(logoutRequestXml, sp, CT.None);
|
||||
var urlEncoded = await EncodeAndSignRequest(logoutRequestXml, sp, _ct);
|
||||
|
||||
// Act
|
||||
var result = await Fixture.Client.GetAsync($"/saml/logout?SAMLRequest={urlEncoded}", CT.None);
|
||||
var result = await Fixture.Client.GetAsync($"/saml/logout?SAMLRequest={urlEncoded}", _ct);
|
||||
|
||||
// Assert
|
||||
var logoutResponse = await ExtractSamlLogoutResponseFromPostAsync(result, CT.None);
|
||||
var logoutResponse = await ExtractSamlLogoutResponseFromPostAsync(result, _ct);
|
||||
logoutResponse.StatusCode.ShouldBe(SamlStatusCodes.Requester);
|
||||
logoutResponse.StatusMessage.ShouldBe("Request has expired (IssueInstant too old)");
|
||||
}
|
||||
|
|
@ -252,13 +254,13 @@ public class SamlSingleLogoutEndpointTests
|
|||
var logoutRequestXml = Build.LogoutRequestXml(
|
||||
destination: new Uri("https://wrong-destination.com/saml/logout"),
|
||||
sessionIndex: "session123");
|
||||
var urlEncoded = await EncodeAndSignRequest(logoutRequestXml, sp, CT.None);
|
||||
var urlEncoded = await EncodeAndSignRequest(logoutRequestXml, sp, _ct);
|
||||
|
||||
// Act
|
||||
var result = await Fixture.Client.GetAsync($"/saml/logout?SAMLRequest={urlEncoded}", CT.None);
|
||||
var result = await Fixture.Client.GetAsync($"/saml/logout?SAMLRequest={urlEncoded}", _ct);
|
||||
|
||||
// Assert
|
||||
var logoutResponse = await ExtractSamlLogoutResponseFromPostAsync(result, CT.None);
|
||||
var logoutResponse = await ExtractSamlLogoutResponseFromPostAsync(result, _ct);
|
||||
logoutResponse.StatusCode.ShouldBe(SamlStatusCodes.Requester);
|
||||
logoutResponse.StatusMessage.ShouldBe($"Invalid destination. Expected '{Fixture.Url()}/saml/logout'");
|
||||
}
|
||||
|
|
@ -276,14 +278,14 @@ public class SamlSingleLogoutEndpointTests
|
|||
var logoutRequestXml = Build.LogoutRequestXml(
|
||||
destination: new Uri($"{Fixture.Url()}/saml/logout"),
|
||||
sessionIndex: "session123");
|
||||
var urlEncoded = await EncodeRequest(logoutRequestXml, CT.None);
|
||||
var urlEncoded = await EncodeRequest(logoutRequestXml, _ct);
|
||||
|
||||
// Act
|
||||
var result = await Fixture.Client.GetAsync($"/saml/logout?SAMLRequest={urlEncoded}", CT.None);
|
||||
var result = await Fixture.Client.GetAsync($"/saml/logout?SAMLRequest={urlEncoded}", _ct);
|
||||
|
||||
// Assert
|
||||
result.StatusCode.ShouldBe(HttpStatusCode.BadRequest);
|
||||
var problemDetails = await result.Content.ReadFromJsonAsync<ProblemDetails>(CT.None);
|
||||
var problemDetails = await result.Content.ReadFromJsonAsync<ProblemDetails>(_ct);
|
||||
problemDetails.ShouldNotBeNull();
|
||||
problemDetails.Detail.ShouldBe($"Service Provider '{sp.EntityId}' has no signing certificates configured and has sent a SAML logout request which requires signature validation");
|
||||
}
|
||||
|
|
@ -302,13 +304,13 @@ public class SamlSingleLogoutEndpointTests
|
|||
var logoutRequestXml = Build.LogoutRequestXml(
|
||||
destination: new Uri($"{Fixture.Url()}/saml/logout"),
|
||||
sessionIndex: "session123");
|
||||
var urlEncoded = await EncodeRequest(logoutRequestXml, CT.None);
|
||||
var urlEncoded = await EncodeRequest(logoutRequestXml, _ct);
|
||||
|
||||
// Act
|
||||
var result = await Fixture.Client.GetAsync($"/saml/logout?SAMLRequest={urlEncoded}", CT.None);
|
||||
var result = await Fixture.Client.GetAsync($"/saml/logout?SAMLRequest={urlEncoded}", _ct);
|
||||
|
||||
// Assert
|
||||
var logoutResponse = await ExtractSamlLogoutResponseFromPostAsync(result, CT.None);
|
||||
var logoutResponse = await ExtractSamlLogoutResponseFromPostAsync(result, _ct);
|
||||
logoutResponse.StatusCode.ShouldBe(SamlStatusCodes.Requester);
|
||||
logoutResponse.StatusMessage.ShouldBe("Missing signature parameter");
|
||||
}
|
||||
|
|
@ -327,13 +329,13 @@ public class SamlSingleLogoutEndpointTests
|
|||
var logoutRequestXml = Build.LogoutRequestXml(
|
||||
notOnOrAfter: expiredTime,
|
||||
sessionIndex: "session123");
|
||||
var urlEncoded = await EncodeAndSignRequest(logoutRequestXml, sp, CT.None);
|
||||
var urlEncoded = await EncodeAndSignRequest(logoutRequestXml, sp, _ct);
|
||||
|
||||
// Act
|
||||
var result = await Fixture.Client.GetAsync($"/saml/logout?SAMLRequest={urlEncoded}", CT.None);
|
||||
var result = await Fixture.Client.GetAsync($"/saml/logout?SAMLRequest={urlEncoded}", _ct);
|
||||
|
||||
// Assert
|
||||
var logoutResponse = await ExtractSamlLogoutResponseFromPostAsync(result, CT.None);
|
||||
var logoutResponse = await ExtractSamlLogoutResponseFromPostAsync(result, _ct);
|
||||
logoutResponse.StatusCode.ShouldBe(SamlStatusCodes.Requester);
|
||||
logoutResponse.StatusMessage.ShouldBe("Logout request expired (NotOnOrAfter is in the past)");
|
||||
}
|
||||
|
|
@ -353,13 +355,13 @@ public class SamlSingleLogoutEndpointTests
|
|||
var logoutRequestXml = Build.LogoutRequestXml(
|
||||
destination: new Uri($"{Fixture.Url()}/saml/logout"),
|
||||
sessionIndex: "session123");
|
||||
var urlEncoded = await EncodeAndSignRequest(logoutRequestXml, sp, CT.None);
|
||||
var urlEncoded = await EncodeAndSignRequest(logoutRequestXml, sp, _ct);
|
||||
|
||||
// Act
|
||||
var result = await Fixture.Client.GetAsync($"/saml/logout?SAMLRequest={urlEncoded}", CT.None);
|
||||
var result = await Fixture.Client.GetAsync($"/saml/logout?SAMLRequest={urlEncoded}", _ct);
|
||||
|
||||
// Assert
|
||||
var logoutResponse = await ExtractSamlLogoutResponseFromPostAsync(result, CT.None);
|
||||
var logoutResponse = await ExtractSamlLogoutResponseFromPostAsync(result, _ct);
|
||||
logoutResponse.StatusCode.ShouldBe(SamlStatusCodes.Success);
|
||||
}
|
||||
|
||||
|
|
@ -379,19 +381,19 @@ public class SamlSingleLogoutEndpointTests
|
|||
// Sign in a user first
|
||||
Fixture.UserToSignIn =
|
||||
new ClaimsPrincipal(new ClaimsIdentity([new Claim(JwtClaimTypes.Subject, "user-id")], "Test"));
|
||||
await Fixture.Client.GetAsync("/__signin", CT.None);
|
||||
await Fixture.Client.GetAsync("/__signin", _ct);
|
||||
|
||||
// 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.Url()}/saml/logout"));
|
||||
var urlEncoded = await EncodeAndSignRequest(logoutRequestXml, sp, CT.None);
|
||||
var urlEncoded = await EncodeAndSignRequest(logoutRequestXml, sp, _ct);
|
||||
|
||||
// Act
|
||||
var result = await Fixture.Client.GetAsync($"/saml/logout?SAMLRequest={urlEncoded}", CT.None);
|
||||
var result = await Fixture.Client.GetAsync($"/saml/logout?SAMLRequest={urlEncoded}", _ct);
|
||||
|
||||
// Assert
|
||||
var logoutResponse = await ExtractSamlLogoutResponseFromPostAsync(result, CT.None);
|
||||
var logoutResponse = await ExtractSamlLogoutResponseFromPostAsync(result, _ct);
|
||||
logoutResponse.StatusCode.ShouldBe(SamlStatusCodes.Success);
|
||||
}
|
||||
|
||||
|
|
@ -408,19 +410,19 @@ public class SamlSingleLogoutEndpointTests
|
|||
// Sign in a user first
|
||||
Fixture.UserToSignIn =
|
||||
new ClaimsPrincipal(new ClaimsIdentity([new Claim(JwtClaimTypes.Subject, "user-id")], "Test"));
|
||||
await Fixture.Client.GetAsync("/__signin", CT.None);
|
||||
await Fixture.Client.GetAsync("/__signin", _ct);
|
||||
|
||||
// Use a different session index than what was established
|
||||
var logoutRequestXml = Build.LogoutRequestXml(
|
||||
destination: new Uri($"{Fixture.Url()}/saml/logout"),
|
||||
sessionIndex: "wrong-session-index");
|
||||
var urlEncoded = await EncodeAndSignRequest(logoutRequestXml, sp, CT.None);
|
||||
var urlEncoded = await EncodeAndSignRequest(logoutRequestXml, sp, _ct);
|
||||
|
||||
// Act
|
||||
var result = await Fixture.Client.GetAsync($"/saml/logout?SAMLRequest={urlEncoded}", CT.None);
|
||||
var result = await Fixture.Client.GetAsync($"/saml/logout?SAMLRequest={urlEncoded}", _ct);
|
||||
|
||||
// Assert
|
||||
var logoutResponse = await ExtractSamlLogoutResponseFromPostAsync(result, CT.None);
|
||||
var logoutResponse = await ExtractSamlLogoutResponseFromPostAsync(result, _ct);
|
||||
logoutResponse.StatusCode.ShouldBe(SamlStatusCodes.Success);
|
||||
}
|
||||
|
||||
|
|
@ -437,7 +439,7 @@ public class SamlSingleLogoutEndpointTests
|
|||
// Sign in a user first
|
||||
Fixture.UserToSignIn =
|
||||
new ClaimsPrincipal(new ClaimsIdentity([new Claim(JwtClaimTypes.Subject, "user-id")], "Test"));
|
||||
await Fixture.Client.GetAsync("/__signin", CT.None);
|
||||
await Fixture.Client.GetAsync("/__signin", _ct);
|
||||
|
||||
// Perform logout to get correct session index from the response
|
||||
var sessionIndex = await PerformSigninAndExtractSessionIndex(sp);
|
||||
|
|
@ -445,10 +447,10 @@ public class SamlSingleLogoutEndpointTests
|
|||
var logoutRequestXml = Build.LogoutRequestXml(
|
||||
destination: new Uri($"{Fixture.Url()}/saml/logout"),
|
||||
sessionIndex: sessionIndex);
|
||||
var urlEncoded = await EncodeAndSignRequest(logoutRequestXml, sp, CT.None);
|
||||
var urlEncoded = await EncodeAndSignRequest(logoutRequestXml, sp, _ct);
|
||||
|
||||
// Act
|
||||
var result = await Fixture.Client.GetAsync($"/saml/logout?SAMLRequest={urlEncoded}", CT.None);
|
||||
var result = await Fixture.Client.GetAsync($"/saml/logout?SAMLRequest={urlEncoded}", _ct);
|
||||
|
||||
// Assert
|
||||
result.StatusCode.ShouldBe(HttpStatusCode.OK);
|
||||
|
|
@ -472,10 +474,10 @@ public class SamlSingleLogoutEndpointTests
|
|||
// Sign in a user first
|
||||
Fixture.UserToSignIn =
|
||||
new ClaimsPrincipal(new ClaimsIdentity([new Claim(JwtClaimTypes.Subject, "user-id")], "Test"));
|
||||
await Fixture.Client.GetAsync("/__signin", CT.None);
|
||||
await Fixture.Client.GetAsync("/__signin", _ct);
|
||||
|
||||
// Ensure user can access protected resource
|
||||
var initialProtectedResourceResult = await Fixture.Client.GetAsync("__protected-resource", CT.None);
|
||||
var initialProtectedResourceResult = await Fixture.Client.GetAsync("__protected-resource", _ct);
|
||||
initialProtectedResourceResult.StatusCode.ShouldBe(HttpStatusCode.OK);
|
||||
|
||||
var sessionIndex = await PerformSigninAndExtractSessionIndex(sp);
|
||||
|
|
@ -483,16 +485,16 @@ public class SamlSingleLogoutEndpointTests
|
|||
var logoutRequestXml = Build.LogoutRequestXml(
|
||||
destination: new Uri($"{Fixture.Url()}/saml/logout"),
|
||||
sessionIndex: sessionIndex);
|
||||
var urlEncoded = await EncodeAndSignRequest(logoutRequestXml, sp, CT.None);
|
||||
var urlEncoded = await EncodeAndSignRequest(logoutRequestXml, sp, _ct);
|
||||
|
||||
// Act
|
||||
var result = await Fixture.Client.GetAsync($"/saml/logout?SAMLRequest={urlEncoded}", CT.None);
|
||||
var result = await Fixture.Client.GetAsync($"/saml/logout?SAMLRequest={urlEncoded}", _ct);
|
||||
|
||||
// Assert
|
||||
result.StatusCode.ShouldBe(HttpStatusCode.OK); // Follows redirect
|
||||
|
||||
// Verify user can no longer access protected resource and is redirected to login
|
||||
var finalProtectedResourceResult = await Fixture.Client.GetAsync("__protected-resource", CT.None);
|
||||
var finalProtectedResourceResult = await Fixture.Client.GetAsync("__protected-resource", _ct);
|
||||
finalProtectedResourceResult.StatusCode.ShouldBe(HttpStatusCode.OK);
|
||||
finalProtectedResourceResult.RequestMessage?.RequestUri?.AbsoluteUri.ShouldStartWith($"{Fixture.Url()}{Fixture.LoginUrl.ToString()}");
|
||||
}
|
||||
|
|
@ -500,7 +502,7 @@ public class SamlSingleLogoutEndpointTests
|
|||
private static async Task<string> EncodeAndSignRequest(
|
||||
string xml,
|
||||
SamlServiceProvider sp,
|
||||
CT ct = default)
|
||||
Ct ct = default)
|
||||
{
|
||||
var encoded = await EncodeRequest(xml, ct);
|
||||
|
||||
|
|
@ -514,9 +516,9 @@ public class SamlSingleLogoutEndpointTests
|
|||
private async Task<string> PerformSigninAndExtractSessionIndex(SamlServiceProvider samlServiceProvider)
|
||||
{
|
||||
var signinRequest = Build.AuthNRequestXml();
|
||||
var encoded = await EncodeAndSignRequest(signinRequest, samlServiceProvider, CT.None);
|
||||
var signinResult = await Fixture.Client.GetAsync($"/saml/signin?SAMLRequest={encoded}", CT.None);
|
||||
var samlResult = await ExtractSamlSuccessFromPostAsync(signinResult, CT.None);
|
||||
var encoded = await EncodeAndSignRequest(signinRequest, samlServiceProvider, _ct);
|
||||
var signinResult = await Fixture.Client.GetAsync($"/saml/signin?SAMLRequest={encoded}", _ct);
|
||||
var samlResult = await ExtractSamlSuccessFromPostAsync(signinResult, _ct);
|
||||
if (string.IsNullOrWhiteSpace(samlResult.Assertion.AuthnStatement?.SessionIndex))
|
||||
{
|
||||
throw new InvalidOperationException("SAMLResult did not have a valid session index");
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ namespace Duende.IdentityServer.IntegrationTests.Endpoints.Saml;
|
|||
|
||||
internal static class SamlTestHelpers
|
||||
{
|
||||
public static async Task<string> EncodeRequest(string authenticationRequest, CT ct = default)
|
||||
public static async Task<string> EncodeRequest(string authenticationRequest, Ct ct = default)
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(authenticationRequest);
|
||||
using var outputStream = new MemoryStream();
|
||||
|
|
@ -37,7 +37,7 @@ internal static class SamlTestHelpers
|
|||
/// <summary>
|
||||
/// Extracts SAML error response from an HTTP-POST binding auto-submit form.
|
||||
/// </summary>
|
||||
public static async Task<SamlErrorResponseData> ExtractSamlErrorFromPostAsync(HttpResponseMessage response, CT ct = default)
|
||||
public static async Task<SamlErrorResponseData> ExtractSamlErrorFromPostAsync(HttpResponseMessage response, Ct ct = default)
|
||||
{
|
||||
var (responseXml, relayState, acsUrl) = await ExtractSamlResponse(response, ct);
|
||||
var (samlpNs, samlNs, responseElement) = ParseSamlResponseXml(responseXml);
|
||||
|
|
@ -58,7 +58,7 @@ internal static class SamlTestHelpers
|
|||
};
|
||||
}
|
||||
|
||||
public static async Task<SamlLogoutResponseData> ExtractSamlLogoutResponseFromPostAsync(HttpResponseMessage response, CT ct = default)
|
||||
public static async Task<SamlLogoutResponseData> ExtractSamlLogoutResponseFromPostAsync(HttpResponseMessage response, Ct ct = default)
|
||||
{
|
||||
response.StatusCode.ShouldBe(HttpStatusCode.OK);
|
||||
|
||||
|
|
@ -81,7 +81,7 @@ internal static class SamlTestHelpers
|
|||
};
|
||||
}
|
||||
|
||||
public static async Task<SamlSuccessResponseData> ExtractSamlSuccessFromPostAsync(HttpResponseMessage response, CT ct = default)
|
||||
public static async Task<SamlSuccessResponseData> ExtractSamlSuccessFromPostAsync(HttpResponseMessage response, Ct ct = default)
|
||||
{
|
||||
var (responseXml, relayState, acsUrl) = await ExtractSamlResponse(response, ct);
|
||||
var (samlpNs, samlNs, responseElement) = ParseSamlResponseXml(responseXml);
|
||||
|
|
@ -105,7 +105,7 @@ internal static class SamlTestHelpers
|
|||
};
|
||||
}
|
||||
|
||||
public static async Task<(string responseXml, string? relayState, string acsUrl)> ExtractSamlResponse(HttpResponseMessage response, CT ct = default)
|
||||
public static async Task<(string responseXml, string? relayState, string acsUrl)> ExtractSamlResponse(HttpResponseMessage response, Ct ct = default)
|
||||
{
|
||||
response.StatusCode.ShouldBe(HttpStatusCode.OK);
|
||||
response.Content.Headers.ContentType?.MediaType.ShouldBe("text/html");
|
||||
|
|
@ -821,7 +821,7 @@ internal static class SamlTestHelpers
|
|||
public static async Task<SamlSuccessResponseData> ExtractAndDecryptSamlSuccessFromPostAsync(
|
||||
HttpResponseMessage response,
|
||||
X509Certificate2 decryptionCertificate,
|
||||
CT ct = default)
|
||||
Ct ct = default)
|
||||
{
|
||||
var (responseXml, relayState, acsUrl) = await ExtractSamlResponse(response, ct);
|
||||
var (samlpNs, samlNs, responseElement) = ParseSamlResponseXml(responseXml);
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@ namespace Duende.IdentityServer.IntegrationTests.Endpoints.Saml;
|
|||
|
||||
internal class SustainSysSamlTestFixture(ITestOutputHelper output) : IAsyncLifetime
|
||||
{
|
||||
private readonly Ct _ct = TestContext.Current.CancellationToken;
|
||||
|
||||
public KestrelTestHost? IdpHost;
|
||||
public KestrelTestHost? SpHost;
|
||||
public HttpClient? BrowserClient;
|
||||
|
|
@ -37,7 +39,7 @@ internal class SustainSysSamlTestFixture(ITestOutputHelper output) : IAsyncLifet
|
|||
{
|
||||
_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($"{IdpHost!.Uri()}/__signin", CT.None);
|
||||
await BrowserClient!.GetAsync($"{IdpHost!.Uri()}/__signin", _ct);
|
||||
}
|
||||
|
||||
public void GenerateSigningCertificate() =>
|
||||
|
|
@ -132,7 +134,7 @@ internal class SustainSysSamlTestFixture(ITestOutputHelper output) : IAsyncLifet
|
|||
ctx.Response.StatusCode = 204;
|
||||
});
|
||||
},
|
||||
CT.None);
|
||||
_ct);
|
||||
}
|
||||
|
||||
private async Task InitializeServiceProvider(string identityProviderHostUri, X509Certificate2? signingCertificate = null) => SpHost = await KestrelTestHost.Create(output,
|
||||
|
|
@ -183,7 +185,7 @@ internal class SustainSysSamlTestFixture(ITestOutputHelper output) : IAsyncLifet
|
|||
await context.Response.WriteAsync(userId.Value, context.RequestAborted);
|
||||
}).RequireAuthorization();
|
||||
},
|
||||
CT.None);
|
||||
_ct);
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ public class SustainSysSigninTests(ITestOutputHelper output)
|
|||
{
|
||||
private const string Category = "SustainSys SAML signin";
|
||||
|
||||
private readonly Ct _ct = TestContext.Current.CancellationToken;
|
||||
|
||||
private SustainSysSamlTestFixture Fixture = new(output);
|
||||
|
||||
[Fact]
|
||||
|
|
@ -91,14 +93,14 @@ public class SustainSysSigninTests(ITestOutputHelper output)
|
|||
// since HttpClient doesn't support JavaScript, we need to extra the content from the auto form post and manually
|
||||
// complete the callback to the Service Provider's ACS URL the same way a user in a browser with JavaScript disabled
|
||||
// would have to manually submit the form
|
||||
var (samlResponse, relayState, acsUrl) = await ExtractSamlResponse(response, CT.None);
|
||||
var (samlResponse, relayState, acsUrl) = await ExtractSamlResponse(response, _ct);
|
||||
var formData = new Dictionary<string, string> { { "SAMLResponse", ConvertToBase64Encoded(samlResponse) } };
|
||||
if (!string.IsNullOrEmpty(relayState))
|
||||
{
|
||||
formData.Add("RelayState", HttpUtility.UrlEncode(relayState));
|
||||
}
|
||||
using var formContent = new FormUrlEncodedContent(formData);
|
||||
var acsResult = await Fixture.BrowserClient!.PostAsync(acsUrl, formContent, CT.None);
|
||||
var acsResult = await Fixture.BrowserClient!.PostAsync(acsUrl, formContent, _ct);
|
||||
|
||||
return acsResult;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ public class MockSamlLogoutNotificationService : ISamlLogoutNotificationService
|
|||
public bool GetSamlFrontChannelLogoutsAsyncCalled { get; set; }
|
||||
public List<ISamlFrontChannelLogout> SamlFrontChannelLogouts { get; set; } = [];
|
||||
|
||||
public Task<IEnumerable<ISamlFrontChannelLogout>> GetSamlFrontChannelLogoutsAsync(LogoutNotificationContext context)
|
||||
public Task<IEnumerable<ISamlFrontChannelLogout>> GetSamlFrontChannelLogoutsAsync(LogoutNotificationContext context, Ct _)
|
||||
{
|
||||
GetSamlFrontChannelLogoutsAsyncCalled = true;
|
||||
return Task.FromResult(SamlFrontChannelLogouts.AsEnumerable());
|
||||
|
|
|
|||
|
|
@ -15,9 +15,9 @@ internal class MockSamlSigningService : ISamlSigningService
|
|||
|
||||
public MockSamlSigningService(X509Certificate2 certificate) => _certificate = certificate;
|
||||
|
||||
public Task<X509Certificate2> GetSigningCertificateAsync() => Task.FromResult(_certificate);
|
||||
public Task<X509Certificate2> GetSigningCertificateAsync(Ct _) => Task.FromResult(_certificate);
|
||||
|
||||
public Task<string> GetSigningCertificateBase64Async()
|
||||
public Task<string> GetSigningCertificateBase64Async(Ct _)
|
||||
{
|
||||
var certBytes = _certificate.Export(X509ContentType.Cert);
|
||||
return Task.FromResult(Convert.ToBase64String(certBytes));
|
||||
|
|
|
|||
|
|
@ -55,16 +55,16 @@ public class MockUserSession : IUserSession
|
|||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task AddSamlSessionAsync(SamlSpSessionData session)
|
||||
public Task AddSamlSessionAsync(SamlSpSessionData session, Ct _)
|
||||
{
|
||||
SamlSessions.RemoveAll(s => s.EntityId == session.EntityId);
|
||||
SamlSessions.Add(session);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<IEnumerable<SamlSpSessionData>> GetSamlSessionListAsync() => Task.FromResult<IEnumerable<SamlSpSessionData>>(SamlSessions);
|
||||
public Task<IEnumerable<SamlSpSessionData>> GetSamlSessionListAsync(Ct _) => Task.FromResult<IEnumerable<SamlSpSessionData>>(SamlSessions);
|
||||
|
||||
public Task RemoveSamlSessionAsync(string entityId)
|
||||
public Task RemoveSamlSessionAsync(string entityId, Ct _)
|
||||
{
|
||||
SamlSessions.RemoveAll(s => s.EntityId == entityId);
|
||||
return Task.CompletedTask;
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ public class SamlClaimsServiceTests
|
|||
{
|
||||
private const string Category = "SAML Claims Service";
|
||||
|
||||
private readonly Ct _ct = TestContext.Current.CancellationToken;
|
||||
|
||||
private readonly SamlOptions _samlOptions;
|
||||
private readonly IOptions<SamlOptions> _options;
|
||||
private readonly MockProfileService _profileService;
|
||||
|
|
@ -51,7 +53,7 @@ public class SamlClaimsServiceTests
|
|||
_profileService.ProfileClaims = user.Claims.ToList();
|
||||
|
||||
// Act
|
||||
var attributes = (await _service.GetMappedAttributesAsync(user, sp)).ToList();
|
||||
var attributes = (await _service.GetMappedAttributesAsync(user, sp, _ct)).ToList();
|
||||
|
||||
// Assert
|
||||
attributes.Count.ShouldBe(3);
|
||||
|
|
@ -105,7 +107,7 @@ public class SamlClaimsServiceTests
|
|||
_profileService.ProfileClaims = user.Claims.ToList();
|
||||
|
||||
// Act
|
||||
var attributes = (await service.GetMappedAttributesAsync(user, sp)).ToList();
|
||||
var attributes = (await service.GetMappedAttributesAsync(user, sp, _ct)).ToList();
|
||||
|
||||
// Assert
|
||||
attributes.Count.ShouldBe(4);
|
||||
|
|
@ -140,7 +142,7 @@ public class SamlClaimsServiceTests
|
|||
_profileService.ProfileClaims = user.Claims.ToList();
|
||||
|
||||
// Act
|
||||
var attributes = (await service.GetMappedAttributesAsync(user, sp)).ToList();
|
||||
var attributes = (await service.GetMappedAttributesAsync(user, sp, _ct)).ToList();
|
||||
|
||||
// Assert
|
||||
attributes.Count.ShouldBe(0); // No mappings, so no attributes
|
||||
|
|
@ -180,7 +182,7 @@ public class SamlClaimsServiceTests
|
|||
_profileService.ProfileClaims = user.Claims.ToList();
|
||||
|
||||
// Act
|
||||
var attributes = (await service.GetMappedAttributesAsync(user, sp)).ToList();
|
||||
var attributes = (await service.GetMappedAttributesAsync(user, sp, _ct)).ToList();
|
||||
|
||||
// Assert
|
||||
attributes.Count.ShouldBe(2); // Only email and department are mapped; sub and unmapped are excluded
|
||||
|
|
@ -217,7 +219,7 @@ public class SamlClaimsServiceTests
|
|||
_profileService.ProfileClaims = user.Claims.ToList();
|
||||
|
||||
// Act
|
||||
var attributes = (await _service.GetMappedAttributesAsync(user, sp)).ToList();
|
||||
var attributes = (await _service.GetMappedAttributesAsync(user, sp, _ct)).ToList();
|
||||
|
||||
// Assert
|
||||
attributes.Count.ShouldBe(2); // email and department from SP mappings; sub not mapped
|
||||
|
|
@ -251,7 +253,7 @@ public class SamlClaimsServiceTests
|
|||
_profileService.ProfileClaims = user.Claims.ToList();
|
||||
|
||||
// Act
|
||||
var attributes = (await _service.GetMappedAttributesAsync(user, sp)).ToList();
|
||||
var attributes = (await _service.GetMappedAttributesAsync(user, sp, _ct)).ToList();
|
||||
|
||||
// Assert
|
||||
attributes.Count.ShouldBe(1); // Only email is mapped (overridden by SP); sub and given_name are not in defaults
|
||||
|
|
@ -292,7 +294,7 @@ public class SamlClaimsServiceTests
|
|||
_profileService.ProfileClaims = user.Claims.ToList();
|
||||
|
||||
// Act
|
||||
var attributes = (await service.GetMappedAttributesAsync(user, sp)).ToList();
|
||||
var attributes = (await service.GetMappedAttributesAsync(user, sp, _ct)).ToList();
|
||||
|
||||
// Assert
|
||||
attributes.Count.ShouldBe(2); // sub + role (multi-valued)
|
||||
|
|
@ -330,7 +332,7 @@ public class SamlClaimsServiceTests
|
|||
_profileService.ProfileClaims = user.Claims.ToList();
|
||||
|
||||
// Act
|
||||
var attributes = (await service.GetMappedAttributesAsync(user, sp)).ToList();
|
||||
var attributes = (await service.GetMappedAttributesAsync(user, sp, _ct)).ToList();
|
||||
|
||||
// Assert
|
||||
attributes.Count.ShouldBe(1);
|
||||
|
|
@ -360,7 +362,7 @@ public class SamlClaimsServiceTests
|
|||
_profileService.ProfileClaims = user.Claims.ToList();
|
||||
|
||||
// Act
|
||||
var attributes = (await _service.GetMappedAttributesAsync(user, sp)).ToList();
|
||||
var attributes = (await _service.GetMappedAttributesAsync(user, sp, _ct)).ToList();
|
||||
|
||||
// Assert
|
||||
attributes.ShouldAllBe(a => a.NameFormat == _samlOptions.DefaultAttributeNameFormat);
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ public class SamlFrontChannelLogoutRequestBuilderTests
|
|||
{
|
||||
private const string Category = "SAML Front Channel Logout Request Builder";
|
||||
|
||||
private readonly Ct _ct = TestContext.Current.CancellationToken;
|
||||
|
||||
private readonly FakeTimeProvider _timeProvider;
|
||||
private readonly SamlProtocolMessageSigner _signer;
|
||||
private readonly SamlFrontChannelLogoutRequestBuilder _subject;
|
||||
|
|
@ -38,7 +40,7 @@ public class SamlFrontChannelLogoutRequestBuilderTests
|
|||
sp.SingleLogoutServiceUrl = null;
|
||||
|
||||
await Should.ThrowAsync<InvalidOperationException>(async () =>
|
||||
await _subject.BuildLogoutRequestAsync(sp, "user@example.com", null, "session123", "https://idp.example.com")
|
||||
await _subject.BuildLogoutRequestAsync(sp, "user@example.com", null, "session123", "https://idp.example.com", _ct)
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -53,7 +55,8 @@ public class SamlFrontChannelLogoutRequestBuilderTests
|
|||
"user@example.com",
|
||||
"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
|
||||
"session123",
|
||||
"https://idp.example.com");
|
||||
"https://idp.example.com",
|
||||
_ct);
|
||||
|
||||
result.SamlBinding.ShouldBe(SamlBinding.HttpRedirect);
|
||||
result.Destination.ShouldBe(sp.SingleLogoutServiceUrl!.Location);
|
||||
|
|
@ -75,7 +78,8 @@ public class SamlFrontChannelLogoutRequestBuilderTests
|
|||
"user@example.com",
|
||||
"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
|
||||
"session123",
|
||||
"https://idp.example.com");
|
||||
"https://idp.example.com",
|
||||
_ct);
|
||||
|
||||
result.SamlBinding.ShouldBe(SamlBinding.HttpPost);
|
||||
}
|
||||
|
|
@ -92,7 +96,7 @@ public class SamlFrontChannelLogoutRequestBuilderTests
|
|||
};
|
||||
|
||||
await Should.ThrowAsync<InvalidOperationException>(async () =>
|
||||
await _subject.BuildLogoutRequestAsync(sp, "user@example.com", null, "session123", "https://idp.example.com")
|
||||
await _subject.BuildLogoutRequestAsync(sp, "user@example.com", null, "session123", "https://idp.example.com", _ct)
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -107,7 +111,8 @@ public class SamlFrontChannelLogoutRequestBuilderTests
|
|||
"user@example.com",
|
||||
"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
|
||||
"session123",
|
||||
"https://idp.example.com");
|
||||
"https://idp.example.com",
|
||||
_ct);
|
||||
|
||||
result.EncodedContent.ShouldNotBeNullOrEmpty();
|
||||
result.EncodedContent.ShouldContain("SAMLRequest=");
|
||||
|
|
@ -126,7 +131,8 @@ public class SamlFrontChannelLogoutRequestBuilderTests
|
|||
"user@example.com",
|
||||
"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
|
||||
"session123",
|
||||
"https://idp.example.com");
|
||||
"https://idp.example.com",
|
||||
_ct);
|
||||
|
||||
var queryString = result.EncodedContent;
|
||||
var samlRequestPart = queryString.Split('&')[0].Replace("?SAMLRequest=", "");
|
||||
|
|
@ -158,7 +164,8 @@ public class SamlFrontChannelLogoutRequestBuilderTests
|
|||
"user@example.com",
|
||||
"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
|
||||
"session123",
|
||||
"https://idp.example.com");
|
||||
"https://idp.example.com",
|
||||
_ct);
|
||||
|
||||
result.EncodedContent.ShouldNotBeNullOrEmpty();
|
||||
|
||||
|
|
@ -181,7 +188,8 @@ public class SamlFrontChannelLogoutRequestBuilderTests
|
|||
"user@example.com",
|
||||
null,
|
||||
"session123",
|
||||
"https://idp.example.com");
|
||||
"https://idp.example.com",
|
||||
_ct);
|
||||
|
||||
var xml = await DecodeRedirectRequest(result.EncodedContent);
|
||||
var expectedIssueInstant = expectedTime.ToString("yyyy-MM-ddTHH:mm:ss.fffZ", CultureInfo.InvariantCulture);
|
||||
|
|
@ -199,7 +207,8 @@ public class SamlFrontChannelLogoutRequestBuilderTests
|
|||
"user@example.com",
|
||||
null,
|
||||
"session123",
|
||||
"https://idp.example.com");
|
||||
"https://idp.example.com",
|
||||
_ct);
|
||||
|
||||
var xml = await DecodeRedirectRequest(result.EncodedContent);
|
||||
xml.ShouldContain($"Destination=\"{sp.SingleLogoutServiceUrl!.Location}\"");
|
||||
|
|
@ -217,7 +226,8 @@ public class SamlFrontChannelLogoutRequestBuilderTests
|
|||
"user@example.com",
|
||||
null,
|
||||
"session123",
|
||||
issuer);
|
||||
issuer,
|
||||
_ct);
|
||||
|
||||
var xml = await DecodeRedirectRequest(result.EncodedContent);
|
||||
xml.ShouldContain($"<Issuer xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\">{issuer}</Issuer>");
|
||||
|
|
@ -235,7 +245,8 @@ public class SamlFrontChannelLogoutRequestBuilderTests
|
|||
nameId,
|
||||
null,
|
||||
"session123",
|
||||
"https://idp.example.com");
|
||||
"https://idp.example.com",
|
||||
_ct);
|
||||
|
||||
var xml = await DecodeRedirectRequest(result.EncodedContent);
|
||||
xml.ShouldContain($"<NameID xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\">{nameId}</NameID>");
|
||||
|
|
@ -253,7 +264,8 @@ public class SamlFrontChannelLogoutRequestBuilderTests
|
|||
"user@example.com",
|
||||
nameIdFormat,
|
||||
"session123",
|
||||
"https://idp.example.com");
|
||||
"https://idp.example.com",
|
||||
_ct);
|
||||
|
||||
var xml = await DecodeRedirectRequest(result.EncodedContent);
|
||||
xml.ShouldContain($"Format=\"{nameIdFormat}\"");
|
||||
|
|
@ -270,7 +282,8 @@ public class SamlFrontChannelLogoutRequestBuilderTests
|
|||
"user@example.com",
|
||||
null,
|
||||
"session123",
|
||||
"https://idp.example.com");
|
||||
"https://idp.example.com",
|
||||
_ct);
|
||||
|
||||
var xml = await DecodeRedirectRequest(result.EncodedContent);
|
||||
var doc = XDocument.Parse(xml);
|
||||
|
|
@ -290,7 +303,8 @@ public class SamlFrontChannelLogoutRequestBuilderTests
|
|||
"user@example.com",
|
||||
null,
|
||||
sessionIndex,
|
||||
"https://idp.example.com");
|
||||
"https://idp.example.com",
|
||||
_ct);
|
||||
|
||||
var xml = await DecodeRedirectRequest(result.EncodedContent);
|
||||
xml.ShouldContain($"<SessionIndex>{sessionIndex}</SessionIndex>");
|
||||
|
|
@ -302,8 +316,8 @@ public class SamlFrontChannelLogoutRequestBuilderTests
|
|||
{
|
||||
var sp = CreateServiceProvider();
|
||||
|
||||
var result1 = await _subject.BuildLogoutRequestAsync(sp, "user@example.com", null, "session123", "https://idp.example.com");
|
||||
var result2 = await _subject.BuildLogoutRequestAsync(sp, "user@example.com", null, "session123", "https://idp.example.com");
|
||||
var result1 = await _subject.BuildLogoutRequestAsync(sp, "user@example.com", null, "session123", "https://idp.example.com", _ct);
|
||||
var result2 = await _subject.BuildLogoutRequestAsync(sp, "user@example.com", null, "session123", "https://idp.example.com", _ct);
|
||||
|
||||
var xml1 = await DecodeRedirectRequest(result1.EncodedContent);
|
||||
var xml2 = await DecodeRedirectRequest(result2.EncodedContent);
|
||||
|
|
@ -327,7 +341,8 @@ public class SamlFrontChannelLogoutRequestBuilderTests
|
|||
"user@example.com",
|
||||
null,
|
||||
"session123",
|
||||
"https://idp.example.com");
|
||||
"https://idp.example.com",
|
||||
_ct);
|
||||
|
||||
var xml = await DecodeRedirectRequest(result.EncodedContent);
|
||||
xml.ShouldContain("Version=\"2.0\"");
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ public class SamlLogoutCallbackProcessorTests
|
|||
{
|
||||
private const string Category = "SAML Logout Callback Processor";
|
||||
|
||||
private readonly Ct _ct = TestContext.Current.CancellationToken;
|
||||
|
||||
private readonly UnitTests.Common.MockMessageStore<LogoutMessage> _logoutMessageStore = new();
|
||||
private readonly MockServiceProviderStore _serviceProviderStore = new();
|
||||
private readonly LogoutResponseBuilder _logoutResponseBuilder;
|
||||
|
|
@ -38,7 +40,7 @@ public class SamlLogoutCallbackProcessorTests
|
|||
[Trait("Category", Category)]
|
||||
public async Task invalid_logout_id_should_return_error()
|
||||
{
|
||||
var result = await _subject.ProcessAsync("invalid", CT.None);
|
||||
var result = await _subject.ProcessAsync("invalid", _ct);
|
||||
|
||||
result.Success.ShouldBeFalse();
|
||||
result.Error.Message.ShouldContain("No logout message found");
|
||||
|
|
@ -56,7 +58,7 @@ public class SamlLogoutCallbackProcessorTests
|
|||
};
|
||||
_logoutMessageStore.Messages["logoutId123"] = new Message<LogoutMessage>(logoutMessage, DateTimeOffset.UtcNow.UtcDateTime);
|
||||
|
||||
var result = await _subject.ProcessAsync("logoutId123", CT.None);
|
||||
var result = await _subject.ProcessAsync("logoutId123", _ct);
|
||||
|
||||
result.Success.ShouldBeFalse();
|
||||
result.Error.Message.ShouldContain("does not contain SAML SP entity ID");
|
||||
|
|
@ -75,7 +77,7 @@ public class SamlLogoutCallbackProcessorTests
|
|||
};
|
||||
_logoutMessageStore.Messages["logoutId123"] = new Message<LogoutMessage>(logoutMessage, DateTimeOffset.UtcNow.UtcDateTime);
|
||||
|
||||
var result = await _subject.ProcessAsync("logoutId123", CT.None);
|
||||
var result = await _subject.ProcessAsync("logoutId123", _ct);
|
||||
|
||||
result.Success.ShouldBeFalse();
|
||||
result.Error.Message.ShouldContain("Service Provider not found");
|
||||
|
|
@ -97,7 +99,7 @@ public class SamlLogoutCallbackProcessorTests
|
|||
};
|
||||
_logoutMessageStore.Messages["logoutId123"] = new Message<LogoutMessage>(logoutMessage, DateTimeOffset.UtcNow.UtcDateTime);
|
||||
|
||||
var result = await _subject.ProcessAsync("logoutId123", CT.None);
|
||||
var result = await _subject.ProcessAsync("logoutId123", _ct);
|
||||
|
||||
result.Success.ShouldBeFalse();
|
||||
result.Error.Message.ShouldContain("is disabled");
|
||||
|
|
@ -119,7 +121,7 @@ public class SamlLogoutCallbackProcessorTests
|
|||
};
|
||||
_logoutMessageStore.Messages["logoutId123"] = new Message<LogoutMessage>(logoutMessage, DateTimeOffset.UtcNow.UtcDateTime);
|
||||
|
||||
var result = await _subject.ProcessAsync("logoutId123", CT.None);
|
||||
var result = await _subject.ProcessAsync("logoutId123", _ct);
|
||||
|
||||
result.Success.ShouldBeFalse();
|
||||
result.Error.Message.ShouldContain("has no SingleLogoutServiceUrl");
|
||||
|
|
@ -140,7 +142,7 @@ public class SamlLogoutCallbackProcessorTests
|
|||
};
|
||||
_logoutMessageStore.Messages["logoutId123"] = new Message<LogoutMessage>(logoutMessage, DateTimeOffset.UtcNow.UtcDateTime);
|
||||
|
||||
var result = await _subject.ProcessAsync("logoutId123", CT.None);
|
||||
var result = await _subject.ProcessAsync("logoutId123", _ct);
|
||||
|
||||
result.Success.ShouldBeFalse();
|
||||
result.Error.Message.ShouldContain("does not contain SAML logout request ID");
|
||||
|
|
@ -162,7 +164,7 @@ public class SamlLogoutCallbackProcessorTests
|
|||
};
|
||||
_logoutMessageStore.Messages["logoutId123"] = new Message<LogoutMessage>(logoutMessage, DateTimeOffset.UtcNow.UtcDateTime);
|
||||
|
||||
var result = await _subject.ProcessAsync("logoutId123", CT.None);
|
||||
var result = await _subject.ProcessAsync("logoutId123", _ct);
|
||||
|
||||
result.Success.ShouldBeTrue();
|
||||
var logoutResponse = result.Value;
|
||||
|
|
@ -187,7 +189,7 @@ public class SamlLogoutCallbackProcessorTests
|
|||
};
|
||||
_logoutMessageStore.Messages["logoutId123"] = new Message<LogoutMessage>(logoutMessage, DateTimeOffset.UtcNow.UtcDateTime);
|
||||
|
||||
var result = await _subject.ProcessAsync("logoutId123", CT.None);
|
||||
var result = await _subject.ProcessAsync("logoutId123", _ct);
|
||||
|
||||
result.Success.ShouldBeTrue();
|
||||
var logoutResponse = result.Value;
|
||||
|
|
@ -211,7 +213,7 @@ public class SamlLogoutCallbackProcessorTests
|
|||
};
|
||||
_logoutMessageStore.Messages["logoutId123"] = new Message<LogoutMessage>(logoutMessage, DateTimeOffset.UtcNow.UtcDateTime);
|
||||
|
||||
var result = await _subject.ProcessAsync("logoutId123", CT.None);
|
||||
var result = await _subject.ProcessAsync("logoutId123", _ct);
|
||||
|
||||
result.Success.ShouldBeTrue();
|
||||
result.Value.RelayState.ShouldBeNull();
|
||||
|
|
@ -232,7 +234,7 @@ public class SamlLogoutCallbackProcessorTests
|
|||
};
|
||||
_logoutMessageStore.Messages["logoutId123"] = new Message<LogoutMessage>(logoutMessage, DateTimeOffset.UtcNow.UtcDateTime);
|
||||
|
||||
var result = await _subject.ProcessAsync("logoutId123", CT.None);
|
||||
var result = await _subject.ProcessAsync("logoutId123", _ct);
|
||||
|
||||
result.Success.ShouldBeTrue();
|
||||
result.Value.Issuer.ShouldBe("https://idp.example.com");
|
||||
|
|
@ -255,7 +257,7 @@ public class SamlLogoutCallbackProcessorTests
|
|||
{
|
||||
public Dictionary<string, SamlServiceProvider> ServiceProviders { get; } = [];
|
||||
|
||||
public Task<SamlServiceProvider?> FindByEntityIdAsync(string entityId)
|
||||
public Task<SamlServiceProvider?> FindByEntityIdAsync(string entityId, Ct _)
|
||||
{
|
||||
ServiceProviders.TryGetValue(entityId, out var sp);
|
||||
return Task.FromResult(sp);
|
||||
|
|
@ -266,6 +268,6 @@ public class SamlLogoutCallbackProcessorTests
|
|||
{
|
||||
public string IssuerName { get; set; } = "https://idp.example.com";
|
||||
|
||||
public Task<string> GetCurrentAsync() => Task.FromResult(IssuerName);
|
||||
public Task<string> GetCurrentAsync(Ct _) => Task.FromResult(IssuerName);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ public class SamlLogoutNotificationServiceTests
|
|||
{
|
||||
private const string Category = "SAML Logout Notification Service";
|
||||
|
||||
private readonly Ct _ct = TestContext.Current.CancellationToken;
|
||||
|
||||
private readonly MockUserSession _userSession = new();
|
||||
private readonly TestIssuerNameService _issuerNameService = new();
|
||||
|
||||
|
|
@ -70,7 +72,7 @@ public class SamlLogoutNotificationServiceTests
|
|||
};
|
||||
var subject = CreateSubject();
|
||||
|
||||
var result = await subject.GetSamlFrontChannelLogoutsAsync(context);
|
||||
var result = await subject.GetSamlFrontChannelLogoutsAsync(context, _ct);
|
||||
|
||||
result.ShouldBeEmpty();
|
||||
}
|
||||
|
|
@ -95,7 +97,7 @@ public class SamlLogoutNotificationServiceTests
|
|||
};
|
||||
var subject = CreateSubject();
|
||||
|
||||
var result = await subject.GetSamlFrontChannelLogoutsAsync(context);
|
||||
var result = await subject.GetSamlFrontChannelLogoutsAsync(context, _ct);
|
||||
|
||||
result.ShouldBeEmpty();
|
||||
}
|
||||
|
|
@ -121,7 +123,7 @@ public class SamlLogoutNotificationServiceTests
|
|||
};
|
||||
var subject = CreateSubject(sp);
|
||||
|
||||
var result = await subject.GetSamlFrontChannelLogoutsAsync(context);
|
||||
var result = await subject.GetSamlFrontChannelLogoutsAsync(context, _ct);
|
||||
|
||||
result.ShouldBeEmpty();
|
||||
}
|
||||
|
|
@ -147,7 +149,7 @@ public class SamlLogoutNotificationServiceTests
|
|||
};
|
||||
var subject = CreateSubject(sp);
|
||||
|
||||
var result = await subject.GetSamlFrontChannelLogoutsAsync(context);
|
||||
var result = await subject.GetSamlFrontChannelLogoutsAsync(context, _ct);
|
||||
|
||||
result.ShouldBeEmpty();
|
||||
}
|
||||
|
|
@ -172,7 +174,7 @@ public class SamlLogoutNotificationServiceTests
|
|||
};
|
||||
var subject = CreateSubject(sp);
|
||||
|
||||
var result = await subject.GetSamlFrontChannelLogoutsAsync(context);
|
||||
var result = await subject.GetSamlFrontChannelLogoutsAsync(context, _ct);
|
||||
|
||||
result.ShouldHaveSingleItem();
|
||||
}
|
||||
|
|
@ -205,7 +207,7 @@ public class SamlLogoutNotificationServiceTests
|
|||
};
|
||||
var subject = CreateSubject(sp1, sp2);
|
||||
|
||||
var result = await subject.GetSamlFrontChannelLogoutsAsync(context);
|
||||
var result = await subject.GetSamlFrontChannelLogoutsAsync(context, _ct);
|
||||
|
||||
result.Count().ShouldBe(2);
|
||||
}
|
||||
|
|
@ -236,7 +238,7 @@ public class SamlLogoutNotificationServiceTests
|
|||
|
||||
var subject = CreateSubject(sp);
|
||||
|
||||
var result = await subject.GetSamlFrontChannelLogoutsAsync(context);
|
||||
var result = await subject.GetSamlFrontChannelLogoutsAsync(context, _ct);
|
||||
|
||||
result.ShouldHaveSingleItem();
|
||||
}
|
||||
|
|
@ -274,7 +276,7 @@ public class SamlLogoutNotificationServiceTests
|
|||
|
||||
var subject = CreateSubject(sp1, sp2);
|
||||
|
||||
var result = await subject.GetSamlFrontChannelLogoutsAsync(context);
|
||||
var result = await subject.GetSamlFrontChannelLogoutsAsync(context, _ct);
|
||||
|
||||
result.Count().ShouldBe(2);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ public class SamlProtocolMessageSignerTests
|
|||
{
|
||||
private const string Category = "SAML Protocol Message Signer";
|
||||
|
||||
private readonly Ct _ct = TestContext.Current.CancellationToken;
|
||||
|
||||
private readonly SamlServiceProvider _samlServiceProvider = new SamlServiceProvider
|
||||
{
|
||||
EntityId = "https://sp.example.com",
|
||||
|
|
@ -75,7 +77,7 @@ public class SamlProtocolMessageSignerTests
|
|||
var signer = CreateSigner();
|
||||
var logoutResponse = CreateLogoutResponseElement();
|
||||
|
||||
var signedXml = await signer.SignProtocolMessage(logoutResponse, _samlServiceProvider);
|
||||
var signedXml = await signer.SignProtocolMessage(logoutResponse, _samlServiceProvider, _ct);
|
||||
|
||||
signedXml.ShouldContain("Signature");
|
||||
signedXml.ShouldContain("SignatureValue");
|
||||
|
|
@ -89,7 +91,7 @@ public class SamlProtocolMessageSignerTests
|
|||
var signer = CreateSigner();
|
||||
var logoutResponse = CreateLogoutResponseElement();
|
||||
|
||||
var signedXml = await signer.SignProtocolMessage(logoutResponse, _samlServiceProvider);
|
||||
var signedXml = await signer.SignProtocolMessage(logoutResponse, _samlServiceProvider, _ct);
|
||||
|
||||
var indexOfIssuer = signedXml.IndexOf("<Issuer", StringComparison.InvariantCulture);
|
||||
var indexOfSignature = signedXml.IndexOf("<Signature", StringComparison.InvariantCulture);
|
||||
|
|
@ -104,7 +106,7 @@ public class SamlProtocolMessageSignerTests
|
|||
var signer = CreateSigner();
|
||||
var logoutResponse = CreateLogoutResponseElement();
|
||||
|
||||
var signedXml = await signer.SignProtocolMessage(logoutResponse, _samlServiceProvider);
|
||||
var signedXml = await signer.SignProtocolMessage(logoutResponse, _samlServiceProvider, _ct);
|
||||
|
||||
signedXml.ShouldContain("http://www.w3.org/2001/04/xmldsig-more#rsa-sha256");
|
||||
signedXml.ShouldContain("http://www.w3.org/2001/04/xmlenc#sha256");
|
||||
|
|
@ -117,7 +119,7 @@ public class SamlProtocolMessageSignerTests
|
|||
var signer = CreateSigner();
|
||||
var logoutResponse = CreateLogoutResponseElement();
|
||||
|
||||
var signedXml = await signer.SignProtocolMessage(logoutResponse, _samlServiceProvider);
|
||||
var signedXml = await signer.SignProtocolMessage(logoutResponse, _samlServiceProvider, _ct);
|
||||
|
||||
signedXml.ShouldContain("KeyInfo");
|
||||
signedXml.ShouldContain("X509Data");
|
||||
|
|
@ -131,7 +133,7 @@ public class SamlProtocolMessageSignerTests
|
|||
var signer = CreateSigner();
|
||||
var queryString = "?SAMLRequest=encodedrequest";
|
||||
|
||||
var signedQueryString = await signer.SignQueryString(queryString);
|
||||
var signedQueryString = await signer.SignQueryString(queryString, _ct);
|
||||
|
||||
signedQueryString.ShouldContain("&SigAlg=");
|
||||
signedQueryString.ShouldContain("&Signature=");
|
||||
|
|
@ -144,7 +146,7 @@ public class SamlProtocolMessageSignerTests
|
|||
var signer = CreateSigner();
|
||||
var queryString = "?SAMLRequest=encodedrequest&RelayState=state123";
|
||||
|
||||
var signedQueryString = await signer.SignQueryString(queryString);
|
||||
var signedQueryString = await signer.SignQueryString(queryString, _ct);
|
||||
|
||||
signedQueryString.ShouldStartWith(queryString);
|
||||
}
|
||||
|
|
@ -156,7 +158,7 @@ public class SamlProtocolMessageSignerTests
|
|||
var signer = CreateSigner();
|
||||
var queryString = "?SAMLRequest=encodedrequest";
|
||||
|
||||
var signedQueryString = await signer.SignQueryString(queryString);
|
||||
var signedQueryString = await signer.SignQueryString(queryString, _ct);
|
||||
|
||||
// The SigAlg parameter should be present
|
||||
signedQueryString.ShouldContain("&SigAlg=");
|
||||
|
|
@ -171,7 +173,7 @@ public class SamlProtocolMessageSignerTests
|
|||
var signer = CreateSigner();
|
||||
var queryString = "?SAMLRequest=encodedrequest";
|
||||
|
||||
var signedQueryString = await signer.SignQueryString(queryString);
|
||||
var signedQueryString = await signer.SignQueryString(queryString, _ct);
|
||||
|
||||
var signaturePart = signedQueryString.Split("&Signature=")[1];
|
||||
var decodedSignature = Uri.UnescapeDataString(signaturePart);
|
||||
|
|
@ -188,7 +190,7 @@ public class SamlProtocolMessageSignerTests
|
|||
var signer = CreateSigner();
|
||||
var queryString = "?SAMLRequest=encodedrequest";
|
||||
|
||||
var signedQueryString = await signer.SignQueryString(queryString);
|
||||
var signedQueryString = await signer.SignQueryString(queryString, _ct);
|
||||
|
||||
// Base64 can contain + and / which should be URL encoded
|
||||
signedQueryString.ShouldNotContain("Signature= "); // No unencoded spaces
|
||||
|
|
@ -202,7 +204,7 @@ public class SamlProtocolMessageSignerTests
|
|||
var signer = CreateSigner();
|
||||
var queryString = "?SAMLRequest=encoded&RelayState=mystate";
|
||||
|
||||
var signedQueryString = await signer.SignQueryString(queryString);
|
||||
var signedQueryString = await signer.SignQueryString(queryString, _ct);
|
||||
|
||||
// SigAlg should come after RelayState but before Signature
|
||||
var sigAlgIndex = signedQueryString.IndexOf("&SigAlg=", StringComparison.Ordinal);
|
||||
|
|
@ -220,8 +222,8 @@ public class SamlProtocolMessageSignerTests
|
|||
var signer = CreateSigner();
|
||||
var queryString = "?SAMLRequest=encodedrequest";
|
||||
|
||||
var signedQueryString1 = await signer.SignQueryString(queryString);
|
||||
var signedQueryString2 = await signer.SignQueryString(queryString);
|
||||
var signedQueryString1 = await signer.SignQueryString(queryString, _ct);
|
||||
var signedQueryString2 = await signer.SignQueryString(queryString, _ct);
|
||||
|
||||
// Signatures should be identical for same input with same key
|
||||
signedQueryString1.ShouldBe(signedQueryString2);
|
||||
|
|
@ -235,8 +237,8 @@ public class SamlProtocolMessageSignerTests
|
|||
var queryString1 = "?SAMLRequest=request1";
|
||||
var queryString2 = "?SAMLRequest=request2";
|
||||
|
||||
var signedQueryString1 = await signer.SignQueryString(queryString1);
|
||||
var signedQueryString2 = await signer.SignQueryString(queryString2);
|
||||
var signedQueryString1 = await signer.SignQueryString(queryString1, _ct);
|
||||
var signedQueryString2 = await signer.SignQueryString(queryString2, _ct);
|
||||
|
||||
// Extract just the signature parts
|
||||
var signature1 = signedQueryString1.Split("&Signature=")[1];
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ public class SamlSigningServiceTests
|
|||
{
|
||||
private const string Category = "SAML Signing Service";
|
||||
|
||||
private readonly Ct _ct = TestContext.Current.CancellationToken;
|
||||
|
||||
private readonly MockKeyMaterialService _mockKeyMaterialService = new();
|
||||
private readonly SamlSigningService _signingService;
|
||||
|
||||
|
|
@ -63,7 +65,7 @@ public class SamlSigningServiceTests
|
|||
_mockKeyMaterialService.SigningCredentials.Add(credentials);
|
||||
|
||||
// Act
|
||||
var result = await _signingService.GetSigningCertificateAsync();
|
||||
var result = await _signingService.GetSigningCertificateAsync(_ct);
|
||||
|
||||
// Assert
|
||||
result.ShouldNotBeNull();
|
||||
|
|
@ -82,7 +84,7 @@ public class SamlSigningServiceTests
|
|||
|
||||
// Act & Assert
|
||||
var ex = await Should.ThrowAsync<InvalidOperationException>(
|
||||
async () => await _signingService.GetSigningCertificateAsync());
|
||||
async () => await _signingService.GetSigningCertificateAsync(_ct));
|
||||
|
||||
ex.Message.ShouldBe("Signing credential must be an X509 certificate with private key.");
|
||||
}
|
||||
|
|
@ -98,7 +100,7 @@ public class SamlSigningServiceTests
|
|||
|
||||
// Act & Assert
|
||||
var ex = await Should.ThrowAsync<InvalidOperationException>(
|
||||
async () => await _signingService.GetSigningCertificateAsync());
|
||||
async () => await _signingService.GetSigningCertificateAsync(_ct));
|
||||
|
||||
ex.Message.ShouldBe("Signing certificate must have a private key.");
|
||||
}
|
||||
|
|
@ -111,7 +113,7 @@ public class SamlSigningServiceTests
|
|||
|
||||
// Act & Assert
|
||||
var ex = await Should.ThrowAsync<InvalidOperationException>(
|
||||
async () => await _signingService.GetSigningCertificateAsync());
|
||||
async () => await _signingService.GetSigningCertificateAsync(_ct));
|
||||
|
||||
ex.Message.ShouldBe("No signing credential available. Configure a signing certificate.");
|
||||
}
|
||||
|
|
@ -126,7 +128,7 @@ public class SamlSigningServiceTests
|
|||
_mockKeyMaterialService.SigningCredentials.Add(credentials);
|
||||
|
||||
// Act
|
||||
var result = await _signingService.GetSigningCertificateBase64Async();
|
||||
var result = await _signingService.GetSigningCertificateBase64Async(_ct);
|
||||
|
||||
// Assert
|
||||
result.ShouldNotBeNullOrEmpty();
|
||||
|
|
@ -150,7 +152,7 @@ public class SamlSigningServiceTests
|
|||
|
||||
// Act & Assert
|
||||
var ex = await Should.ThrowAsync<InvalidOperationException>(
|
||||
async () => await _signingService.GetSigningCertificateBase64Async());
|
||||
async () => await _signingService.GetSigningCertificateBase64Async(_ct));
|
||||
|
||||
ex.Message.ShouldBe("Signing credential key is not an X509SecurityKey and cannot be used to extract an X509 certificate for SAML metadata.");
|
||||
}
|
||||
|
|
@ -163,7 +165,7 @@ public class SamlSigningServiceTests
|
|||
|
||||
// Act & Assert
|
||||
var ex = await Should.ThrowAsync<InvalidOperationException>(
|
||||
async () => await _signingService.GetSigningCertificateBase64Async());
|
||||
async () => await _signingService.GetSigningCertificateBase64Async(_ct));
|
||||
|
||||
ex.Message.ShouldBe("No signing credential available. Configure a signing certificate.");
|
||||
}
|
||||
|
|
@ -178,7 +180,7 @@ public class SamlSigningServiceTests
|
|||
_mockKeyMaterialService.SigningCredentials.Add(credentials);
|
||||
|
||||
// Act
|
||||
var result = await _signingService.GetSigningCertificateBase64Async();
|
||||
var result = await _signingService.GetSigningCertificateBase64Async(_ct);
|
||||
var bytes = Convert.FromBase64String(result);
|
||||
var exportedCert = X509CertificateLoader.LoadCertificate(bytes);
|
||||
|
||||
|
|
|
|||
|
|
@ -188,7 +188,7 @@ public class DefaultIdentityServerInteractionServiceTests
|
|||
NameId = "user123"
|
||||
});
|
||||
|
||||
var context = await _subject.CreateLogoutContextAsync();
|
||||
var context = await _subject.CreateLogoutContextAsync(_ct);
|
||||
|
||||
context.ShouldNotBeNull();
|
||||
_mockLogoutMessageStore.Messages.ShouldNotBeEmpty();
|
||||
|
|
@ -218,7 +218,7 @@ public class DefaultIdentityServerInteractionServiceTests
|
|||
NameId = "user123"
|
||||
});
|
||||
|
||||
var context = await _subject.CreateLogoutContextAsync();
|
||||
var context = await _subject.CreateLogoutContextAsync(_ct);
|
||||
|
||||
context.ShouldNotBeNull();
|
||||
_mockLogoutMessageStore.Messages.ShouldNotBeEmpty();
|
||||
|
|
@ -242,7 +242,7 @@ public class DefaultIdentityServerInteractionServiceTests
|
|||
_mockUserSession.SessionId = "session";
|
||||
_mockUserSession.Clients.Add("client1");
|
||||
|
||||
var context = await _subject.CreateLogoutContextAsync();
|
||||
var context = await _subject.CreateLogoutContextAsync(_ct);
|
||||
|
||||
context.ShouldNotBeNull();
|
||||
_mockLogoutMessageStore.Messages.ShouldNotBeEmpty();
|
||||
|
|
|
|||
|
|
@ -195,7 +195,7 @@ public class EndSessionRequestValidatorTests
|
|||
|
||||
var parameters = new NameValueCollection();
|
||||
|
||||
var result = await _subject.ValidateAsync(parameters, _user);
|
||||
var result = await _subject.ValidateAsync(parameters, _user, _ct);
|
||||
|
||||
result.IsError.ShouldBeFalse();
|
||||
result.ValidatedRequest.SamlSessions.ShouldNotBeNull();
|
||||
|
|
@ -212,7 +212,7 @@ public class EndSessionRequestValidatorTests
|
|||
|
||||
var parameters = new NameValueCollection();
|
||||
|
||||
var result = await _subject.ValidateAsync(parameters, _user);
|
||||
var result = await _subject.ValidateAsync(parameters, _user, _ct);
|
||||
|
||||
result.IsError.ShouldBeFalse();
|
||||
result.ValidatedRequest.SamlSessions.ShouldNotBeNull();
|
||||
|
|
@ -232,7 +232,7 @@ public class EndSessionRequestValidatorTests
|
|||
|
||||
var parameters = new NameValueCollection();
|
||||
|
||||
var result = await _subject.ValidateAsync(parameters, _user);
|
||||
var result = await _subject.ValidateAsync(parameters, _user, _ct);
|
||||
|
||||
result.IsError.ShouldBeFalse();
|
||||
|
||||
|
|
@ -267,7 +267,7 @@ public class EndSessionRequestValidatorTests
|
|||
var parameters = new NameValueCollection();
|
||||
parameters.Add("id_token_hint", "id_token");
|
||||
|
||||
var result = await _subject.ValidateAsync(parameters, _user);
|
||||
var result = await _subject.ValidateAsync(parameters, _user, _ct);
|
||||
|
||||
result.IsError.ShouldBeFalse();
|
||||
result.ValidatedRequest.SamlSessions.ShouldNotBeNull();
|
||||
|
|
@ -303,7 +303,7 @@ public class EndSessionRequestValidatorTests
|
|||
{ "endSessionId", "endSessionId123" }
|
||||
};
|
||||
|
||||
var result = await _subject.ValidateCallbackAsync(parameters);
|
||||
var result = await _subject.ValidateCallbackAsync(parameters, _ct);
|
||||
|
||||
result.IsError.ShouldBeFalse();
|
||||
result.SamlFrontChannelLogouts.ShouldNotBeNull();
|
||||
|
|
@ -340,7 +340,7 @@ public class EndSessionRequestValidatorTests
|
|||
{ "endSessionId", "endSessionId123" }
|
||||
};
|
||||
|
||||
var result = await _subject.ValidateCallbackAsync(parameters);
|
||||
var result = await _subject.ValidateCallbackAsync(parameters, _ct);
|
||||
|
||||
result.IsError.ShouldBeFalse();
|
||||
result.FrontChannelLogoutUrls.ShouldHaveSingleItem();
|
||||
|
|
@ -364,7 +364,7 @@ public class EndSessionRequestValidatorTests
|
|||
{ "endSessionId", "endSessionId123" }
|
||||
};
|
||||
|
||||
var result = await _subject.ValidateCallbackAsync(parameters);
|
||||
var result = await _subject.ValidateCallbackAsync(parameters, _ct);
|
||||
|
||||
result.IsError.ShouldBeTrue();
|
||||
}
|
||||
|
|
@ -403,7 +403,7 @@ public class EndSessionRequestValidatorTests
|
|||
{ "endSessionId", "endSessionId123" }
|
||||
};
|
||||
|
||||
await _subject.ValidateCallbackAsync(parameters);
|
||||
await _subject.ValidateCallbackAsync(parameters, _ct);
|
||||
|
||||
_mockSamlLogoutNotificationService.GetSamlFrontChannelLogoutsAsyncCalled.ShouldBeTrue();
|
||||
}
|
||||
|
|
@ -451,7 +451,7 @@ public class EndSessionRequestValidatorTests
|
|||
{ "endSessionId", "endSessionId123" }
|
||||
};
|
||||
|
||||
var result = await _subject.ValidateCallbackAsync(parameters);
|
||||
var result = await _subject.ValidateCallbackAsync(parameters, _ct);
|
||||
|
||||
result.IsError.ShouldBeFalse();
|
||||
result.SamlFrontChannelLogouts.Count().ShouldBe(3);
|
||||
|
|
|
|||
Loading…
Reference in a new issue