diff --git a/WebApps/Proxy/Microsoft.SPID.Proxy/Models/LoggingEvents.cs b/WebApps/Proxy/Microsoft.SPID.Proxy/Models/LoggingEvents.cs index f1e8894..0ac1277 100644 --- a/WebApps/Proxy/Microsoft.SPID.Proxy/Models/LoggingEvents.cs +++ b/WebApps/Proxy/Microsoft.SPID.Proxy/Models/LoggingEvents.cs @@ -17,6 +17,8 @@ public static class LoggingEvents public const int PROXY_INDEX_INVOKED = 5006; public const int INCOMING_SAML_RESPONSE_DECODED = 5007; public const int ALTERED_DATEOFBIRTH_TYPE = 5008; + public const int ADDED_SAMLRESPONSEID_ATTRIBUTE = 5009; + public const int ADDED_AUTHNCONTEXTCLASSREF_ATTRIBUTE = 5010; diff --git a/WebApps/Proxy/Microsoft.SPID.Proxy/Models/Options/OptionalResponseAlterationOptions.cs b/WebApps/Proxy/Microsoft.SPID.Proxy/Models/Options/OptionalResponseAlterationOptions.cs index 468ef26..d25c3bf 100644 --- a/WebApps/Proxy/Microsoft.SPID.Proxy/Models/Options/OptionalResponseAlterationOptions.cs +++ b/WebApps/Proxy/Microsoft.SPID.Proxy/Models/Options/OptionalResponseAlterationOptions.cs @@ -9,5 +9,13 @@ public class OptionalResponseAlterationOptions { public bool AlterDateOfBirth { get; set; } public string DateOfBirthFormat { get; set; } = "xs:date"; - } + + public bool AddSAMLResponseIDAttribute { get; set; } + public string SAMLResponseIDAttributeName { get; set; } = "OriginalSAMLResponseID"; + public string SAMLResponseIDAttributeXsiType { get; set; } = "xs:string"; + + public bool AddAuthnContextClassRefAttribute { get; set; } + public string AuthnContextClassRefAttributeName { get; set; } = "SPIDLevel"; + public string AuthnContextClassRefAttributeXsiType { get; set; } = "xs:string"; + } } diff --git a/WebApps/Proxy/Microsoft.SPID.Proxy/Services/Implementations/FederatorResponseService.cs b/WebApps/Proxy/Microsoft.SPID.Proxy/Services/Implementations/FederatorResponseService.cs index 28a98e5..5c19156 100644 --- a/WebApps/Proxy/Microsoft.SPID.Proxy/Services/Implementations/FederatorResponseService.cs +++ b/WebApps/Proxy/Microsoft.SPID.Proxy/Services/Implementations/FederatorResponseService.cs @@ -256,8 +256,24 @@ public async Task SignAssertionAsync(XmlDocument doc, string assertionDigestMeth public void ApplyOptionalResponseAlteration(XmlDocument doc) { - if (!_optionalResponseAlterationOptions.AlterDateOfBirth) return; + if (_optionalResponseAlterationOptions.AlterDateOfBirth) + { + ApplyDateOfBirthAlteration(doc); + } + + if (_optionalResponseAlterationOptions.AddSAMLResponseIDAttribute) + { + ApplySAMLResponseIDAttributeAlteration(doc); + } + + if (_optionalResponseAlterationOptions.AddAuthnContextClassRefAttribute) + { + ApplyAuthnContextClassRefAttributeAlteration(doc); + } + } + private void ApplyDateOfBirthAlteration(XmlDocument doc) + { var dateOfBirthNode = FindDateOfBirthNode(doc); if (dateOfBirthNode == null) { @@ -275,6 +291,121 @@ public void ApplyOptionalResponseAlteration(XmlDocument doc) SetTypeAttribute(doc, attributeValueNode); } + private void ApplySAMLResponseIDAttributeAlteration(XmlDocument doc) + { + var responseIdAttr = doc.DocumentElement?.Attributes["ID"]; + if (responseIdAttr == null || string.IsNullOrWhiteSpace(responseIdAttr.Value)) + { + _logger.LogDebug("SAML Response ID not found; skipping SAMLResponseID attribute addition."); + return; + } + + var attributeName = _optionalResponseAlterationOptions.SAMLResponseIDAttributeName; + if (AddSamlAttribute(doc, attributeName, responseIdAttr.Value, _optionalResponseAlterationOptions.SAMLResponseIDAttributeXsiType)) + { + _logger.LogInformation(LoggingEvents.ADDED_SAMLRESPONSEID_ATTRIBUTE, + "Added SAML attribute {attributeName} with the SAML Response ID value.", attributeName); + } + } + + private void ApplyAuthnContextClassRefAttributeAlteration(XmlDocument doc) + { + var authnContextClassRefNode = doc.GetElementsByTagName("AuthnContextClassRef", "*").Cast().FirstOrDefault(); + if (authnContextClassRefNode == null || string.IsNullOrWhiteSpace(authnContextClassRefNode.InnerText)) + { + _logger.LogDebug("AuthnContextClassRef not found; skipping AuthnContextClassRef attribute addition."); + return; + } + + var attributeName = _optionalResponseAlterationOptions.AuthnContextClassRefAttributeName; + if (AddSamlAttribute(doc, attributeName, authnContextClassRefNode.InnerText, _optionalResponseAlterationOptions.AuthnContextClassRefAttributeXsiType)) + { + _logger.LogInformation(LoggingEvents.ADDED_AUTHNCONTEXTCLASSREF_ATTRIBUTE, + "Added SAML attribute {attributeName} with the AuthnContextClassRef value.", attributeName); + } + } + + private bool AddSamlAttribute(XmlDocument doc, string attributeName, string attributeValue, string xsiType) + { + var attributeStatement = doc.GetElementsByTagName("AttributeStatement", "*").Cast().FirstOrDefault(); + if (attributeStatement == null) + { + _logger.LogDebug("AttributeStatement not found; cannot add SAML attribute {attributeName}.", attributeName); + return false; + } + + // Use an existing saml:Attribute as a template so that the new attribute uses the same + // element prefix, namespace URI and attribute structure already present in the SAML Response. + var existingAttribute = attributeStatement.ChildNodes.Cast() + .FirstOrDefault(n => n.NodeType == XmlNodeType.Element && n.LocalName == "Attribute"); + + if (existingAttribute == null) + { + _logger.LogDebug("No existing saml:Attribute found to use as template; cannot add SAML attribute {attributeName}.", attributeName); + return false; + } + + // Clone the template attribute so we preserve every aspect of its shape + // (prefix, namespace declarations, NameFormat attribute, AttributeValue prefix, etc.). + var attributeElement = (XmlElement)existingAttribute.CloneNode(true); + + // Replace the Name attribute value. + var nameAttr = attributeElement.Attributes["Name"]; + if (nameAttr == null) + { + nameAttr = doc.CreateAttribute("Name"); + attributeElement.Attributes.Append(nameAttr); + } + nameAttr.Value = attributeName; + + // Overwrite FriendlyName as well if the template carried one, so the cloned + // attribute doesn't keep the template's friendly name. + var friendlyNameAttr = attributeElement.Attributes["FriendlyName"]; + if (friendlyNameAttr != null) + { + friendlyNameAttr.Value = attributeName; + } + + var attributeValueElement = attributeElement.ChildNodes.Cast() + .FirstOrDefault(n => n.NodeType == XmlNodeType.Element && n.LocalName == "AttributeValue") as XmlElement; + + if (attributeValueElement == null) + { + _logger.LogDebug("Template saml:Attribute has no AttributeValue child; cannot add SAML attribute {attributeName}.", attributeName); + return false; + } + + // Remove any additional AttributeValue elements that came from the template so the + // cloned attribute ends up with exactly one AttributeValue holding the new value. + var extraAttributeValues = attributeElement.ChildNodes.Cast() + .Where(n => n.NodeType == XmlNodeType.Element && n.LocalName == "AttributeValue" && !ReferenceEquals(n, attributeValueElement)) + .ToList(); + foreach (var extra in extraAttributeValues) + { + attributeElement.RemoveChild(extra); + } + + // Replace inner text/children with the new value. + attributeValueElement.RemoveAll(); + attributeValueElement.InnerText = attributeValue; + + // Only set xsi:type if the template AttributeValue already had one, using the value supplied by the caller. + var existingTemplateValue = existingAttribute.ChildNodes.Cast() + .FirstOrDefault(n => n.NodeType == XmlNodeType.Element && n.LocalName == "AttributeValue"); + var templateTypeAttr = existingTemplateValue?.Attributes?.Cast() + .FirstOrDefault(a => a.LocalName == "type" && a.NamespaceURI == "http://www.w3.org/2001/XMLSchema-instance"); + + if (templateTypeAttr != null && !string.IsNullOrWhiteSpace(xsiType)) + { + var typeAttr = doc.CreateAttribute(templateTypeAttr.Prefix, "type", templateTypeAttr.NamespaceURI); + typeAttr.Value = xsiType; + attributeValueElement.Attributes.Append(typeAttr); + } + + attributeStatement.AppendChild(attributeElement); + return true; + } + private XmlNode FindDateOfBirthNode(XmlDocument doc) { var attributes = doc.GetElementsByTagName("Attribute", "*");