diff --git a/AspNetSaml/Saml.cs b/AspNetSaml/Saml.cs index 46981e6..5e315c8 100644 --- a/AspNetSaml/Saml.cs +++ b/AspNetSaml/Saml.cs @@ -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 @@ -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; } @@ -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 @@ -218,6 +235,26 @@ public List GetCustomAttributeAsList(string attr) XmlNodeList nodes = _xmlDoc.SelectNodes("/samlp:Response/saml:Assertion[1]/saml:AttributeStatement/saml:Attribute[@Name='" + attr + "']/saml:AttributeValue", _xmlNameSpaceManager); return nodes?.Cast().Select(x => x.InnerText).ToList(); } + + public List GetIntendedAudiences() + { + XmlNodeList nodes = _xmlDoc.SelectNodes("/samlp:Response/saml:Assertion[1]/saml:Conditions/saml:AudienceRestriction/saml:Audience", _xmlNameSpaceManager); + return nodes?.Cast().Select(x => x.InnerText).ToList(); + } + + /// + /// Some IdPs set the SessionNotOnOrAfter attribute to inform the SP how long they expect the local session with the SP to be valid for. + /// + 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; + } } /// @@ -266,12 +303,19 @@ public string GetSessionIndex() return node?.InnerText; } - /// - /// 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. - /// - 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; + } + + /// + /// 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. + /// + 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.