diff --git a/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSecurityTokenHandler.cs b/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSecurityTokenHandler.cs
index 1be8c0f1c7..e65e8bc09d 100644
--- a/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSecurityTokenHandler.cs
+++ b/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSecurityTokenHandler.cs
@@ -1275,8 +1275,7 @@ private ClaimsPrincipal ValidateToken(SamlSecurityToken samlToken, string token,
ValidateConditions(samlToken, validationParameters);
var issuer = ValidateIssuer(samlToken.Issuer, samlToken, validationParameters);
- if (samlToken.Assertion.Conditions != null)
- ValidateTokenReplay(samlToken.Assertion.Conditions.NotOnOrAfter, samlToken.Assertion.CanonicalString, validationParameters);
+ ValidateTokenReplay(samlToken.Assertion.Conditions?.NotOnOrAfter, samlToken.Assertion.CanonicalString, validationParameters);
ValidateIssuerSecurityKey(samlToken.SigningKey, samlToken, validationParameters);
validatedToken = samlToken;
diff --git a/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2SecurityTokenHandler.cs b/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2SecurityTokenHandler.cs
index 2475ef8041..68b8a0703a 100644
--- a/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2SecurityTokenHandler.cs
+++ b/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2SecurityTokenHandler.cs
@@ -274,8 +274,7 @@ private ClaimsPrincipal ValidateToken(Saml2SecurityToken samlToken, string token
ValidateSubject(samlToken, validationParameters);
var issuer = ValidateIssuer(samlToken.Issuer, samlToken, validationParameters);
- if (samlToken.Assertion.Conditions != null)
- ValidateTokenReplay(samlToken.Assertion.Conditions.NotOnOrAfter, samlToken.Assertion.CanonicalString, validationParameters);
+ ValidateTokenReplay(samlToken.Assertion.Conditions?.NotOnOrAfter, samlToken.Assertion.CanonicalString, validationParameters);
ValidateIssuerSecurityKey(samlToken.SigningKey, samlToken, validationParameters);
validatedToken = samlToken;
diff --git a/test/Microsoft.IdentityModel.TestUtils/ReferenceTokens.cs b/test/Microsoft.IdentityModel.TestUtils/ReferenceTokens.cs
index 52b1d217bf..65381f4787 100644
--- a/test/Microsoft.IdentityModel.TestUtils/ReferenceTokens.cs
+++ b/test/Microsoft.IdentityModel.TestUtils/ReferenceTokens.cs
@@ -83,6 +83,9 @@ public class ReferenceTokens
public static string Saml2Token_NoConditions_NoSignature =
@"https://sts.windows.net/add29489-7269-41f4-8841-b63c95564420/RrX3SPSxDw6z4KHaKB2V_mnv0G-LbRZdYvo1RQa1L7sadd29489-7269-41f4-8841-b63c95564420d1ad9ce7-b322-4221-ab74-1e1011e1bbcbUser1@Cyrano.onmicrosoft.com1UserUser1https://sts.windows.net/add29489-7269-41f4-8841-b63c95564420/urn:oasis:names:tc:SAML:2.0:ac:classes:Password";
+ public static string Saml2Token_ConditionsNoExpiration_NoSignature =
+ @"https://sts.windows.net/add29489-7269-41f4-8841-b63c95564420/RrX3SPSxDw6z4KHaKB2V_mnv0G-LbRZdYvo1RQa1L7sadd29489-7269-41f4-8841-b63c95564420d1ad9ce7-b322-4221-ab74-1e1011e1bbcbUser1@Cyrano.onmicrosoft.com1UserUser1https://sts.windows.net/add29489-7269-41f4-8841-b63c95564420/urn:oasis:names:tc:SAML:2.0:ac:classes:Password";
+
public static string Saml2Token_Actor_Claim =
@"https://sts.windows.net/add29489-7269-41f4-8841-b63c95564420/RrX3SPSxDw6z4KHaKB2V_mnv0G-LbRZdYvo1RQa1L7sspn:fe78e0b4-6fe7-47e6-812c-fb75cee266a4add29489-7269-41f4-8841-b63c95564420d1ad9ce7-b322-4221-ab74-1e1011e1bbcbUser1@Cyrano.onmicrosoft.com1UserUser1https://sts.windows.net/add29489-7269-41f4-8841-b63c95564420/TestActorTestActorurn:oasis:names:tc:SAML:2.0:ac:classes:Password";
@@ -201,6 +204,9 @@ public static string Saml2Token_Formated
public static string SamlToken_NoConditions_NoSignature =
@"Boburn:oasis:names:tc:SAML:1.0:cm:bearerUSABob@contoso.comBob555.1212DeveloperSalesJean-Sébastien";
+ public static string SamlToken_ConditionsNoExpiration_NoSignature =
+ @"Boburn:oasis:names:tc:SAML:1.0:cm:bearerUSABob@contoso.comBob555.1212DeveloperSalesJean-Sébastien";
+
public static string SamlToken_Valid =
@"http://Default.Audience.comBoburn:oasis:names:tc:SAML:1.0:cm:bearerUSABob@contoso.comBob555.1212DeveloperSalesJean-Sébastienz+4mubehpI9sLNVwGGIuy3jiGhP9k+PHiRyO0Mqd/YM=hWd3ALlvaNuHz2JbCnSuVly/pZwtVZzLQwsMvvn03dl6URoFQhvYpldE+6ZpzL77XMrsC0VmPaQbw76fkztK2P/0tp4hzaW///Jdr/E+HcSfG0Cdt+NWuybyGkJljbh0tif6BbxIaDNc/5dx6SoCyItP3IqU5JciwaTsOGQTmcUzoI3lIY9N7pv2ChD3fczWo1O8W+T0Caka69cxhb037HpUWLmjekED9sqBKfLKDsVH8rpef7cGroTILaZ4xOHwOEmrV6xOrCq3erupnLhw5eVC4wDuhL/0KrbfgfTExM5iUnyfrnm+C74M6WnnfqpzWHWuCv10W32W1L8mlQtVZQ==MIIDJTCCAg2gAwIBAgIQGzlg2gNmfKRKBa6dqqZXxzANBgkqhkiG9w0BAQQFADAiMSAwHgYDVQQDExdLZXlTdG9yZVRlc3RDZXJ0aWZpY2F0ZTAeFw0xMTExMDkxODE5MDZaFw0zOTEyMzEyMzU5NTlaMCIxIDAeBgNVBAMTF0tleVN0b3JlVGVzdENlcnRpZmljYXRlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAns1cm8RU1hKZILPI6pB5Zoxn9mW2tSS0atV+o9FCn9NyeOktEOj1kEXOeIz0KfnqxgPMF1GpshuZBAhgjkyy2kNGE6Zx50CCJgq6XUatvVVJpMp8/FV18ynPf+/TRlF8V2HO3IVJ0XqRJ9fGA2f5xpOweWsdLYitdHbaDCl6IBNSXo52iNuqWAcB1k7jBlsnlXpuvslhLIzj60dnghAVA4ltS3NlFyw1Tz3pGlZQDt7x83IBHe7DA9bV3aJs1trkm1NzI1HoRS4vOqU3n4fn+DlfAE2vYKNkSi/PjuAX+1YQCq6e5uN/hOeSEqji8SsWC2nk/bMTKPwD67rn3jNC9wIDAQABo1cwVTBTBgNVHQEETDBKgBA3gSuALjvEuAVmF/x8knXvoSQwIjEgMB4GA1UEAxMXS2V5U3RvcmVUZXN0Q2VydGlmaWNhdGWCEBs5YNoDZnykSgWunaqmV8cwDQYJKoZIhvcNAQEEBQADggEBAFZvDA7PBh/vvFZb/QCBelTyD2Yqij16v3tk30A3Akli6UIILdbbOcA5BiPktT1kJxcsgSXNHUODlfG2Fy9HTqwunr8G7FYniOUXPVrRL+HwhKOzRFDMUS3+On+ZDzum7rbpm3SYlnJDyNb8wynPw/bXQw72jGjt63uh6OnkYE8fJ8iPfVWOenZkP/IXPIXK/bBwLMDJ1y77ZauPYbp7oiQ/991pn0c7F4ugT9LYmbAdJKhiainOaoBTvIHN8/lMZ8gHUuxvOJhPrbgo3NTqvT1/3kfD0AISP4R3pH0QL/0m7cO34nK4rFFLZs1sFUguYUJhfkyq1N8MiyyAqRmrvBQ=";
diff --git a/test/Microsoft.IdentityModel.Tokens.Saml.Tests/SamlConditionsHandlingTests.cs b/test/Microsoft.IdentityModel.Tokens.Saml.Tests/SamlConditionsHandlingTests.cs
new file mode 100644
index 0000000000..f71c5702fc
--- /dev/null
+++ b/test/Microsoft.IdentityModel.Tokens.Saml.Tests/SamlConditionsHandlingTests.cs
@@ -0,0 +1,226 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using Microsoft.IdentityModel.TestUtils;
+using Microsoft.IdentityModel.Tokens.Saml2;
+using Microsoft.IdentityModel.Tokens.Saml2.Tests;
+using Xunit;
+
+#pragma warning disable CS3016 // Arrays as attribute arguments is not CLS-compliant
+
+namespace Microsoft.IdentityModel.Tokens.Saml.Tests
+{
+ ///
+ /// Verifies that the SAML and SAML2 handlers invoke ValidateTokenReplay
+ /// uniformly, regardless of whether the assertion contains a Conditions element.
+ ///
+ public class SamlConditionsHandlingTests
+ {
+ // ———————————————————— SAML 2.0 ————————————————————
+
+ [Theory, MemberData(nameof(Saml2ConditionsHandlingTheoryData), DisableDiscoveryEnumeration = true)]
+ public void Saml2ConditionsHandling(Saml2TheoryData theoryData)
+ {
+ var context = TestUtilities.WriteHeader($"{this}.Saml2ConditionsHandling", theoryData);
+ try
+ {
+ theoryData.Handler.ValidateToken(theoryData.Token, theoryData.ValidationParameters, out SecurityToken validatedToken);
+ theoryData.ExpectedException.ProcessNoException(context);
+ }
+ catch (Exception ex)
+ {
+ theoryData.ExpectedException.ProcessException(ex, context);
+ }
+
+ TestUtilities.AssertFailIfErrors(context);
+ }
+
+ public static TheoryData Saml2ConditionsHandlingTheoryData
+ {
+ get
+ {
+ return new TheoryData
+ {
+ // Conditions absent + replay cache configured: replay validation
+ // still runs and surfaces the missing expiration.
+ new Saml2TheoryData("Saml2_NoConditions_WithReplayCache_ThrowsNoExpiration")
+ {
+ Token = ReferenceTokens.Saml2Token_NoConditions_NoSignature,
+ ExpectedException = ExpectedException.SecurityTokenNoExpirationException("IDX10227:"),
+ ValidationParameters = new TokenValidationParameters
+ {
+ RequireSignedTokens = false,
+ RequireAudience = false,
+ ValidateLifetime = false,
+ ValidateIssuer = false,
+ ValidateAudience = false,
+ ValidateTokenReplay = true,
+ TokenReplayCache = new TokenReplayCache
+ {
+ OnAddReturnValue = true,
+ OnFindReturnValue = false
+ }
+ }
+ },
+ // Token with Conditions and a replay cache continues to validate normally.
+ new Saml2TheoryData("Saml2_WithConditions_WithReplayCache_Succeeds")
+ {
+ Token = ReferenceTokens.Saml2Token_Valid,
+ ValidationParameters = new TokenValidationParameters
+ {
+ IssuerSigningKey = KeyingMaterial.DefaultAADSigningKey,
+ ValidateIssuer = false,
+ ValidateAudience = false,
+ ValidateLifetime = false,
+ ValidateTokenReplay = true,
+ TokenReplayCache = new TokenReplayCache
+ {
+ OnAddReturnValue = true,
+ OnFindReturnValue = false
+ }
+ }
+ },
+ // No Conditions and no replay cache: validation succeeds because
+ // replay enforcement is not requested.
+ new Saml2TheoryData("Saml2_NoConditions_NoReplayCache_Succeeds")
+ {
+ Token = ReferenceTokens.Saml2Token_NoConditions_NoSignature,
+ ValidationParameters = new TokenValidationParameters
+ {
+ RequireSignedTokens = false,
+ RequireAudience = false,
+ ValidateLifetime = false,
+ ValidateIssuer = false,
+ ValidateAudience = false,
+ }
+ },
+ // Conditions element present but NotOnOrAfter is unset — replay validation
+ // still runs and surfaces the missing expiration.
+ new Saml2TheoryData("Saml2_ConditionsNoExpiration_WithReplayCache_ThrowsNoExpiration")
+ {
+ Token = ReferenceTokens.Saml2Token_ConditionsNoExpiration_NoSignature,
+ ExpectedException = ExpectedException.SecurityTokenNoExpirationException("IDX10227:"),
+ ValidationParameters = new TokenValidationParameters
+ {
+ RequireSignedTokens = false,
+ RequireAudience = false,
+ ValidateLifetime = false,
+ ValidateIssuer = false,
+ ValidateAudience = false,
+ ValidateTokenReplay = true,
+ TokenReplayCache = new TokenReplayCache
+ {
+ OnAddReturnValue = true,
+ OnFindReturnValue = false
+ }
+ }
+ },
+ };
+ }
+ }
+
+ // ———————————————————— SAML 1.x ————————————————————
+
+ [Theory, MemberData(nameof(SamlConditionsHandlingTheoryData), DisableDiscoveryEnumeration = true)]
+ public void SamlConditionsHandling(SamlTheoryData theoryData)
+ {
+ var context = TestUtilities.WriteHeader($"{this}.SamlConditionsHandling", theoryData);
+ try
+ {
+ (theoryData.Handler as SamlSecurityTokenHandler).ValidateToken(theoryData.Token, theoryData.ValidationParameters, out SecurityToken validatedToken);
+ theoryData.ExpectedException.ProcessNoException(context);
+ }
+ catch (Exception ex)
+ {
+ theoryData.ExpectedException.ProcessException(ex, context);
+ }
+
+ TestUtilities.AssertFailIfErrors(context);
+ }
+
+ public static TheoryData SamlConditionsHandlingTheoryData
+ {
+ get
+ {
+ return new TheoryData
+ {
+ // Conditions absent + replay cache configured: replay validation
+ // still runs and surfaces the missing expiration.
+ new SamlTheoryData("Saml_NoConditions_WithReplayCache_ThrowsNoExpiration")
+ {
+ Token = ReferenceTokens.SamlToken_NoConditions_NoSignature,
+ ExpectedException = ExpectedException.SecurityTokenNoExpirationException("IDX10227:"),
+ ValidationParameters = new TokenValidationParameters
+ {
+ RequireSignedTokens = false,
+ RequireAudience = false,
+ ValidateLifetime = false,
+ ValidateIssuer = false,
+ ValidateAudience = false,
+ ValidateTokenReplay = true,
+ TokenReplayCache = new TokenReplayCache
+ {
+ OnAddReturnValue = true,
+ OnFindReturnValue = false
+ }
+ }
+ },
+ // Token with Conditions and a replay cache continues to validate normally.
+ new SamlTheoryData("Saml_WithConditions_WithReplayCache_Succeeds")
+ {
+ Token = ReferenceTokens.SamlToken_Valid,
+ ValidationParameters = new TokenValidationParameters
+ {
+ IssuerSigningKey = KeyingMaterial.DefaultX509SigningCreds_2048_RsaSha2_Sha2.Key,
+ ValidateIssuer = false,
+ ValidateAudience = false,
+ ValidateLifetime = false,
+ ValidateTokenReplay = true,
+ TokenReplayCache = new TokenReplayCache
+ {
+ OnAddReturnValue = true,
+ OnFindReturnValue = false
+ }
+ }
+ },
+ // No Conditions and no replay cache: validation succeeds because
+ // replay enforcement is not requested.
+ new SamlTheoryData("Saml_NoConditions_NoReplayCache_Succeeds")
+ {
+ Token = ReferenceTokens.SamlToken_NoConditions_NoSignature,
+ ValidationParameters = new TokenValidationParameters
+ {
+ RequireSignedTokens = false,
+ RequireAudience = false,
+ ValidateLifetime = false,
+ ValidateIssuer = false,
+ ValidateAudience = false,
+ }
+ },
+ // Conditions element present but NotOnOrAfter is unset. SAML 1.x
+ // defaults to DateTime.MaxValue here, so replay validation succeeds
+ // with a far-future expiration.
+ new SamlTheoryData("Saml_ConditionsNoExpiration_WithReplayCache_Succeeds")
+ {
+ Token = ReferenceTokens.SamlToken_ConditionsNoExpiration_NoSignature,
+ ValidationParameters = new TokenValidationParameters
+ {
+ RequireSignedTokens = false,
+ RequireAudience = false,
+ ValidateLifetime = false,
+ ValidateIssuer = false,
+ ValidateAudience = false,
+ ValidateTokenReplay = true,
+ TokenReplayCache = new TokenReplayCache
+ {
+ OnAddReturnValue = true,
+ OnFindReturnValue = false
+ }
+ }
+ },
+ };
+ }
+ }
+ }
+}