Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions WebApps/Proxy/Microsoft.SPID.Proxy/Models/LoggingEvents.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;



Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand All @@ -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<XmlNode>().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);
}
Comment thread
fume marked this conversation as resolved.
}

private bool AddSamlAttribute(XmlDocument doc, string attributeName, string attributeValue, string xsiType)
{
var attributeStatement = doc.GetElementsByTagName("AttributeStatement", "*").Cast<XmlNode>().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<XmlNode>()
.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<XmlNode>()
.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<XmlNode>()
.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;
Comment thread
fume marked this conversation as resolved.

// Only set xsi:type if the template AttributeValue already had one, using the value supplied by the caller.
var existingTemplateValue = existingAttribute.ChildNodes.Cast<XmlNode>()
.FirstOrDefault(n => n.NodeType == XmlNodeType.Element && n.LocalName == "AttributeValue");
var templateTypeAttr = existingTemplateValue?.Attributes?.Cast<XmlAttribute>()
.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);
Comment thread
fume marked this conversation as resolved.
typeAttr.Value = xsiType;
attributeValueElement.Attributes.Append(typeAttr);
}
Comment on lines +392 to +403

attributeStatement.AppendChild(attributeElement);
return true;
}

private XmlNode FindDateOfBirthNode(XmlDocument doc)
{
var attributes = doc.GetElementsByTagName("Attribute", "*");
Expand Down
Loading