Skip to content
Open
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
72 changes: 58 additions & 14 deletions AspNetSaml/Saml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,18 @@ public void LoadXmlFromBase64(string response)
LoadXml(Encoding.UTF8.GetString(Convert.FromBase64String(response)));
}

private XmlElement GetAssertionElement()
{
return _xmlDoc.SelectSingleNode("/samlp:Response/saml:Assertion", _xmlNameSpaceManager) as XmlElement;
}

public string GetAssertionID()
{
var assertionElement = GetAssertionElement();

return assertionElement?.GetAttribute("ID");
}

//an XML signature can "cover" not the whole document, but only a part of it
//.NET's built in "CheckSignature" does not cover this case, it will validate to true.
//We should check the signature reference, so it "references" the id of the root document element! If not - it's a hack
Expand All @@ -83,7 +95,7 @@ protected bool ValidateSignatureReference(SignedXml signedXml)
return true;
else //sometimes its not the "root" doc-element that is being signed, but the "assertion" element
{
var assertionNode = _xmlDoc.SelectSingleNode("/samlp:Response/saml:Assertion", _xmlNameSpaceManager) as XmlElement;
var assertionNode = GetAssertionElement();
if (assertionNode != idElement)
return false;
}
Expand Down Expand Up @@ -119,15 +131,20 @@ public bool IsValid()
return ValidateSignatureReference(signedXml) && signedXml.CheckSignature(_certificate, true) && !IsExpired();
}

public virtual DateTime GetExpirationDate()
{
DateTime expirationDate = DateTime.MaxValue;
XmlNode node = _xmlDoc.SelectSingleNode("/samlp:Response/saml:Assertion[1]/saml:Subject/saml:SubjectConfirmation/saml:SubjectConfirmationData", _xmlNameSpaceManager);
if (node != null && node.Attributes["NotOnOrAfter"] != null) {
DateTime.TryParse(node.Attributes["NotOnOrAfter"].Value, out expirationDate);
}
return expirationDate;
}

protected virtual bool IsExpired()
{
DateTime expirationDate = DateTime.MaxValue;
XmlNode node = _xmlDoc.SelectSingleNode("/samlp:Response/saml:Assertion[1]/saml:Subject/saml:SubjectConfirmation/saml:SubjectConfirmationData", _xmlNameSpaceManager);
if (node != null && node.Attributes["NotOnOrAfter"] != null)
{
DateTime.TryParse(node.Attributes["NotOnOrAfter"].Value, out expirationDate);
}
return (CurrentTime ?? DateTime.UtcNow) > expirationDate.ToUniversalTime();
var expirationDate = GetExpirationDate();
return (CurrentTime ?? DateTime.UtcNow) > expirationDate.ToUniversalTime();
}

public DateTime? CurrentTime { get; set; } = null; //mostly for unit-testing. STUPID I KNOW, will fix later
Expand Down Expand Up @@ -218,6 +235,26 @@ public List<string> GetCustomAttributeAsList(string attr)
XmlNodeList nodes = _xmlDoc.SelectNodes("/samlp:Response/saml:Assertion[1]/saml:AttributeStatement/saml:Attribute[@Name='" + attr + "']/saml:AttributeValue", _xmlNameSpaceManager);
return nodes?.Cast<XmlNode>().Select(x => x.InnerText).ToList();
}

public List<string> GetIntendedAudiences()
{
XmlNodeList nodes = _xmlDoc.SelectNodes("/samlp:Response/saml:Assertion[1]/saml:Conditions/saml:AudienceRestriction/saml:Audience", _xmlNameSpaceManager);
return nodes?.Cast<XmlNode>().Select(x => x.InnerText).ToList();
}

/// <summary>
/// Some IdPs set the SessionNotOnOrAfter attribute to inform the SP how long they expect the local session with the SP to be valid for.
/// </summary>
public DateTime? GetExpectedSessionExpiry()
{
XmlNode node = _xmlDoc.SelectSingleNode("/samlp:Response/saml:Assertion[1]/saml:AuthnStatement", _xmlNameSpaceManager);
if (node != null && node.Attributes["SessionNotOnOrAfter"] != null) {
if (DateTime.TryParse(node.Attributes["SessionNotOnOrAfter"].Value, out var expirationDate)) {
return expirationDate;
}
}
return null;
}
}

/// <summary>
Expand Down Expand Up @@ -266,12 +303,19 @@ public string GetSessionIndex()
return node?.InnerText;
}

/// <summary>
/// Checks the validity of the SAML IdP-initiated LogoutRequest (validate signature).
/// This class relies on the base IsValid() method but overrides IsExpired() to always return false,
/// effectively bypassing the expiration check which is not relevant for LogoutRequests.
/// </summary>
protected override bool IsExpired()
public override DateTime GetExpirationDate()
{
// LogoutRequests don't have the standard expiration elements.
// Return DateTime.MaxValue to indicate no expiration.
return DateTime.MaxValue;
}

/// <summary>
/// Checks the validity of the SAML IdP-initiated LogoutRequest (validate signature).
/// This class relies on the base IsValid() method but overrides IsExpired() to always return false,
/// effectively bypassing the expiration check which is not relevant for LogoutRequests.
/// </summary>
protected override bool IsExpired()
{
// LogoutRequests don't have the standard expiration elements.
// Return false to ensure the base IsValid() check doesn't fail due to expiration.
Expand Down