diff --git a/src/Microsoft.IdentityModel.Tokens.Saml/PublicAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Tokens.Saml/PublicAPI.Unshipped.txt
index e69de29bb2..199337805f 100644
--- a/src/Microsoft.IdentityModel.Tokens.Saml/PublicAPI.Unshipped.txt
+++ b/src/Microsoft.IdentityModel.Tokens.Saml/PublicAPI.Unshipped.txt
@@ -0,0 +1 @@
+const Microsoft.IdentityModel.Tokens.Saml2.Saml2Constants.Attributes.SubjectConfirmationDataType = "Type" -> string
\ No newline at end of file
diff --git a/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2Constants.cs b/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2Constants.cs
index d9eb4e13ca..f5282c6e6f 100644
--- a/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2Constants.cs
+++ b/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2Constants.cs
@@ -75,7 +75,17 @@ public static class Attributes
public const string SessionNotOnOrAfter = "SessionNotOnOrAfter";
public const string SPNameQualifier = "SPNameQualifier";
public const string SPProvidedID = "SPProvidedID";
+ ///
+ /// W3C XML Schema standard xsi:type attribute name (lowercase)
+ ///
public const string Type = "type";
+
+ ///
+ /// SAML specific xsi:type attribute (uppercase).
+ /// Used only for SubjectConfirmationData to maintain compatibility with ADFS and other SAML implementations.
+ /// See: https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/issues/2894
+ ///
+ public const string SubjectConfirmationDataType = "Type";
public const string Version = "Version";
}
diff --git a/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2Serializer.cs b/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2Serializer.cs
index 87511d4f33..1c8cb751b1 100644
--- a/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2Serializer.cs
+++ b/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2Serializer.cs
@@ -2169,7 +2169,8 @@ protected virtual void WriteSubjectConfirmationData(XmlWriter writer, Saml2Subje
// @xsi:type
if (subjectConfirmationData.KeyInfos.Count > 0)
- writer.WriteAttributeString(XmlSignatureConstants.Attributes.Type, XmlSignatureConstants.XmlSchemaNamespace, Saml2Constants.Types.KeyInfoConfirmationDataType);
+ // Use uppercase "Type" specifically for SAML SubjectConfirmationData for ADFS compatibility
+ writer.WriteAttributeString(Saml2Constants.Attributes.SubjectConfirmationDataType, Saml2Constants.Types.KeyInfoConfirmationDataType);
// @Address - optional
if (!string.IsNullOrEmpty(subjectConfirmationData.Address))
diff --git a/test/Microsoft.IdentityModel.Tokens.Saml.Tests/Saml2SerializerTests.cs b/test/Microsoft.IdentityModel.Tokens.Saml.Tests/Saml2SerializerTests.cs
index 47da4c6bba..d7767cd6c5 100644
--- a/test/Microsoft.IdentityModel.Tokens.Saml.Tests/Saml2SerializerTests.cs
+++ b/test/Microsoft.IdentityModel.Tokens.Saml.Tests/Saml2SerializerTests.cs
@@ -626,11 +626,62 @@ public Saml2Subject ReadSubjectPublic(XmlDictionaryReader reader)
return base.ReadSubject(reader);
}
+ public void WriteSubjectConfirmationDataPublic(XmlWriter writer, Saml2SubjectConfirmationData subjectConfirmationData)
+ {
+ base.WriteSubjectConfirmationData(writer, subjectConfirmationData);
+ }
+
public void WriteProxyRestrictionPublic(XmlWriter writer, Saml2ProxyRestriction proxyRestriction)
{
base.WriteProxyRestriction(writer, proxyRestriction);
}
}
+
+ [Theory, MemberData(nameof(WriteSubjectConfirmationDataTheoryData), DisableDiscoveryEnumeration = true)]
+ public void WriteSubjectConfirmationData(Saml2TheoryData theoryData)
+ {
+ TestUtilities.WriteHeader($"{this}.WriteSubjectConfirmationData", theoryData);
+ var context = new CompareContext($"{this}.WriteSubjectConfirmationData, {theoryData.TestId}");
+ try
+ {
+ var ms = new MemoryStream();
+ var writer = XmlDictionaryWriter.CreateTextWriter(ms, Encoding.UTF8, false);
+ (theoryData.Saml2Serializer as Saml2SerializerPublic).WriteSubjectConfirmationDataPublic(writer, theoryData.SubjectConfirmationData);
+
+ writer.Flush();
+ var xml = Encoding.UTF8.GetString(ms.ToArray());
+ IdentityComparer.AreEqual(xml, theoryData.Xml, context);
+ theoryData.ExpectedException.ProcessNoException();
+ }
+ catch (Exception ex)
+ {
+ theoryData.ExpectedException.ProcessException(ex);
+ }
+
+ TestUtilities.AssertFailIfErrors(context);
+ }
+
+ public static TheoryData WriteSubjectConfirmationDataTheoryData
+ {
+ get
+ {
+ var keyInfo = new KeyInfo();
+ keyInfo.KeyName = "test";
+ var confirmationData = new Saml2SubjectConfirmationData();
+ confirmationData.KeyInfos.Add(keyInfo);
+
+ return new TheoryData
+ {
+ new Saml2TheoryData
+ {
+ SubjectConfirmationData = confirmationData,
+ Xml = "test",
+ Saml2Serializer = new Saml2SerializerPublic(),
+ TestId = "WriteSubjectConfirmationDataWithUppercaseType"
+ }
+ };
+ }
+ }
}
}
diff --git a/test/Microsoft.IdentityModel.Tokens.Saml.Tests/Saml2TheoryData.cs b/test/Microsoft.IdentityModel.Tokens.Saml.Tests/Saml2TheoryData.cs
index eef6e370e1..effc2ff2e7 100644
--- a/test/Microsoft.IdentityModel.Tokens.Saml.Tests/Saml2TheoryData.cs
+++ b/test/Microsoft.IdentityModel.Tokens.Saml.Tests/Saml2TheoryData.cs
@@ -57,5 +57,7 @@ public Saml2TheoryData(TokenTheoryData tokenTheoryData)
public Saml2Subject Subject { get; set; }
public Saml2ProxyRestriction ProxyRestriction { get; set; }
+
+ public Saml2SubjectConfirmationData SubjectConfirmationData { get; set; }
}
}