Updated SAML code to participate in cooperative cancellation

This commit is contained in:
Brett Hazen 2026-02-26 09:14:53 -06:00
parent 65afc5d0b1
commit 6a0a2f4a32
60 changed files with 520 additions and 458 deletions

View file

@ -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;

View file

@ -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");

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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)
{

View file

@ -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);
}

View file

@ -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);

View file

@ -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.");
}

View file

@ -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);

View file

@ -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>());
}

View file

@ -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)
{

View file

@ -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
{

View file

@ -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

View file

@ -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));

View file

@ -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));

View file

@ -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);

View file

@ -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);
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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)
{

View file

@ -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);

View file

@ -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);
}

View file

@ -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)
{

View file

@ -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);

View file

@ -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>

View file

@ -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);

View file

@ -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();

View file

@ -34,7 +34,7 @@ internal class SamlSigninEndpoint(
internal async Task<IEndpointResult> ProcessSpInitiatedSignin(
SamlSigninRequest signinRequest,
CT ct = default)
Ct ct = default)
{
logger.StartSamlSigninRequest(LogLevel.Debug);

View file

@ -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
{

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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);

View file

@ -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);

View file

@ -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

View file

@ -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);
}

View file

@ -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!);

View file

@ -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();

View file

@ -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();
}

View file

@ -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)
{

View file

@ -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);
}

View file

@ -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)

View file

@ -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");

View file

@ -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();

View file

@ -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);

View file

@ -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");

View file

@ -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);

View file

@ -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()
{

View file

@ -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;
}

View file

@ -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());

View file

@ -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));

View file

@ -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;

View file

@ -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);

View file

@ -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\"");

View file

@ -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);
}
}

View file

@ -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);
}

View file

@ -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];

View file

@ -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);

View file

@ -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();

View file

@ -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);