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