From c66d04ed1eb9de2f831ed30821148bb5af83562d Mon Sep 17 00:00:00 2001 From: tristan Date: Thu, 25 Jul 2024 14:15:11 -0400 Subject: [PATCH] testing logout functionality --- README.md | 1 + src/AssertionParser.cs | 18 ++++++++++ src/AuthnRequest.cs | 35 +++++++++--------- src/CoreSaml2Utils.csproj | 2 +- src/LogoutRequest.cs | 34 +++++++++--------- src/LogoutResponse.cs | 35 ++++++++---------- src/RequestBase.cs | 23 ++++++++++++ src/Utilities/CertificateUtilities.cs | 13 +++---- src/Utilities/SamlSignedXml.cs | 25 +++++++++++++ src/Utilities/SigningHelper.cs | 52 +++++++++++++++++++++++++++ 10 files changed, 175 insertions(+), 63 deletions(-) create mode 100644 src/Utilities/SamlSignedXml.cs create mode 100644 src/Utilities/SigningHelper.cs diff --git a/README.md b/README.md index 908a632..ce49dbd 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ # CoreSaml2Utils > forked from https://github.com/jitbit/AspNetSaml +> some snippets leveraged from https://github.com/optiklab/SAML-integration-utilities Started from the Jitbit repo but had a need for more advanced concepts like decryption and signing so wound up refactoring a bunch as I went. Became too much of a deviation to PR at this point. Published to nuget, linked above. diff --git a/src/AssertionParser.cs b/src/AssertionParser.cs index 8ed8cae..4e2fb26 100644 --- a/src/AssertionParser.cs +++ b/src/AssertionParser.cs @@ -22,6 +22,21 @@ XmlNamespaceManager xmlNamespaceManager _xmlNameSpaceManager = xmlNamespaceManager; } + public enum RequestType + { + AuthnRequest, + LogoutRequest, + Unknown + } + + public RequestType ResolveRequestType() + => _xmlDoc.DocumentElement?.LocalName switch + { + "AuthnRequest" => RequestType.AuthnRequest, + "LogoutRequest" => RequestType.LogoutRequest, + _ => RequestType.Unknown + }; + public bool IsValid(string expectedAudience, X509Certificate2 idpCert) { if (idpCert == null) @@ -53,6 +68,9 @@ public string GetResponseIssuer() return node?.InnerText; } + public string GetRequestId() + => _xmlDoc.DocumentElement!.Attributes["ID"]?.Value; + public string GetNameID() { var node = SelectSingleNode($"{XPaths.FirstAssertion}/saml:Subject/saml:NameID"); diff --git a/src/AuthnRequest.cs b/src/AuthnRequest.cs index caa424e..3defad5 100644 --- a/src/AuthnRequest.cs +++ b/src/AuthnRequest.cs @@ -1,4 +1,3 @@ -using System; using System.IO; using System.Security.Cryptography.X509Certificates; using System.Xml; @@ -31,24 +30,26 @@ protected override string BuildRequestXml() }; using var stringWriter = new StringWriter(); - using var xmlWriter = XmlWriter.Create(stringWriter, xmlWriterSettings); - xmlWriter.WriteStartElement("samlp", "AuthnRequest", "urn:oasis:names:tc:SAML:2.0:protocol"); - xmlWriter.WriteAttributeString("ID", $"_{Guid.NewGuid()}"); - xmlWriter.WriteAttributeString("Version", "2.0"); - xmlWriter.WriteAttributeString("IssueInstant", BuildIssueInstant()); - xmlWriter.WriteAttributeString("ProtocolBinding", "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"); - xmlWriter.WriteAttributeString("AssertionConsumerServiceURL", _assertionConsumerServiceUrl); - xmlWriter.WriteAttributeString("Destination", RequestDestination); + using (var xmlWriter = XmlWriter.Create(stringWriter, xmlWriterSettings)) + { + xmlWriter.WriteStartElement("samlp", "AuthnRequest", "urn:oasis:names:tc:SAML:2.0:protocol"); + xmlWriter.WriteAttributeString("ID", Id); + xmlWriter.WriteAttributeString("Version", "2.0"); + xmlWriter.WriteAttributeString("IssueInstant", BuildIssueInstant()); + xmlWriter.WriteAttributeString("ProtocolBinding", "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"); + xmlWriter.WriteAttributeString("AssertionConsumerServiceURL", _assertionConsumerServiceUrl); + xmlWriter.WriteAttributeString("Destination", RequestDestination); - xmlWriter.WriteStartElement("saml", "Issuer", "urn:oasis:names:tc:SAML:2.0:assertion"); - xmlWriter.WriteString(Issuer); - xmlWriter.WriteEndElement(); + xmlWriter.WriteStartElement("saml", "Issuer", "urn:oasis:names:tc:SAML:2.0:assertion"); + xmlWriter.WriteString(Issuer); + xmlWriter.WriteEndElement(); - xmlWriter.WriteStartElement("samlp", "NameIDPolicy", "urn:oasis:names:tc:SAML:2.0:protocol"); - xmlWriter.WriteAttributeString("Format", "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"); - xmlWriter.WriteAttributeString("AllowCreate", "true"); - xmlWriter.WriteEndElement(); - xmlWriter.WriteEndElement(); + xmlWriter.WriteStartElement("samlp", "NameIDPolicy", "urn:oasis:names:tc:SAML:2.0:protocol"); + xmlWriter.WriteAttributeString("Format", "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"); + xmlWriter.WriteAttributeString("AllowCreate", "true"); + xmlWriter.WriteEndElement(); + xmlWriter.WriteEndElement(); + } return stringWriter.ToString(); } diff --git a/src/CoreSaml2Utils.csproj b/src/CoreSaml2Utils.csproj index d70b91f..cfa141d 100644 --- a/src/CoreSaml2Utils.csproj +++ b/src/CoreSaml2Utils.csproj @@ -21,7 +21,7 @@ preview - + all runtime; build; native; contentfiles; analyzers diff --git a/src/LogoutRequest.cs b/src/LogoutRequest.cs index 8bbdf71..2d11b24 100644 --- a/src/LogoutRequest.cs +++ b/src/LogoutRequest.cs @@ -1,4 +1,3 @@ -using System; using System.IO; using System.Security.Cryptography.X509Certificates; using System.Xml; @@ -31,21 +30,24 @@ protected override string BuildRequestXml() }; using var stringWriter = new StringWriter(); - using var xmlWriter = XmlWriter.Create(stringWriter, xmlWriterSettings); - xmlWriter.WriteStartElement("samlp", "LogoutRequest", "urn:oasis:names:tc:SAML:2.0:protocol"); - xmlWriter.WriteAttributeString("ID", $"_{Guid.NewGuid()}"); - xmlWriter.WriteAttributeString("Version", "2.0"); - xmlWriter.WriteAttributeString("IssueInstant", BuildIssueInstant()); - - xmlWriter.WriteStartElement("saml", "Issuer", "urn:oasis:names:tc:SAML:2.0:assertion"); - xmlWriter.WriteString(Issuer); - xmlWriter.WriteEndElement(); - - xmlWriter.WriteStartElement("saml", "NameID", "urn:oasis:names:tc:SAML:2.0:assertion"); - xmlWriter.WriteString(_nameId); - xmlWriter.WriteEndElement(); - - xmlWriter.WriteEndElement(); + using (var xmlWriter = XmlWriter.Create(stringWriter, xmlWriterSettings)) + { + xmlWriter.WriteStartElement("samlp", "LogoutRequest", "urn:oasis:names:tc:SAML:2.0:protocol"); + xmlWriter.WriteAttributeString("ID", Id); + xmlWriter.WriteAttributeString("Version", "2.0"); + xmlWriter.WriteAttributeString("IssueInstant", BuildIssueInstant()); + xmlWriter.WriteAttributeString("Destination", RequestDestination); + + xmlWriter.WriteStartElement("saml", "Issuer", "urn:oasis:names:tc:SAML:2.0:assertion"); + xmlWriter.WriteString(Issuer); + xmlWriter.WriteEndElement(); + + xmlWriter.WriteStartElement("saml", "NameID", "urn:oasis:names:tc:SAML:2.0:assertion"); + xmlWriter.WriteString(_nameId); + xmlWriter.WriteEndElement(); + + xmlWriter.WriteEndElement(); + } return stringWriter.ToString(); } diff --git a/src/LogoutResponse.cs b/src/LogoutResponse.cs index b0c6950..59f5b72 100644 --- a/src/LogoutResponse.cs +++ b/src/LogoutResponse.cs @@ -1,4 +1,3 @@ -using System; using System.IO; using System.Security.Cryptography.X509Certificates; using System.Xml; @@ -12,13 +11,12 @@ public class LogoutResponse : RequestBase public LogoutResponse( string issuer, - string requestDestination, string inResponseToId, string status = "urn:oasis:names:tc:SAML:2.0:status:Success", X509Certificate2 cert = null ) : base( issuer, - requestDestination, + null, cert ) { @@ -34,28 +32,25 @@ protected override string BuildRequestXml() }; using var stringWriter = new StringWriter(); - using var xmlWriter = XmlWriter.Create(stringWriter, xmlWriterSettings); - xmlWriter.WriteStartElement("samlp", "LogoutResponse", "urn:oasis:names:tc:SAML:2.0:protocol"); - xmlWriter.WriteAttributeString("ID", $"_{Guid.NewGuid()}"); - xmlWriter.WriteAttributeString("Version", "2.0"); - xmlWriter.WriteAttributeString("IssueInstant", BuildIssueInstant()); - - if (!string.IsNullOrEmpty(RequestDestination)) + using (var xmlWriter = XmlWriter.Create(stringWriter, xmlWriterSettings)) { - xmlWriter.WriteAttributeString("Destination", RequestDestination); - } + xmlWriter.WriteStartElement("samlp", "LogoutResponse", "urn:oasis:names:tc:SAML:2.0:protocol"); + xmlWriter.WriteAttributeString("ID", Id); + xmlWriter.WriteAttributeString("Version", "2.0"); + xmlWriter.WriteAttributeString("IssueInstant", BuildIssueInstant()); - xmlWriter.WriteAttributeString("InResponseTo", _inResponseToId); + xmlWriter.WriteAttributeString("InResponseTo", _inResponseToId); - xmlWriter.WriteStartElement("saml", "Issuer", "urn:oasis:names:tc:SAML:2.0:assertion"); - xmlWriter.WriteString(Issuer); - xmlWriter.WriteEndElement(); + xmlWriter.WriteStartElement("saml", "Issuer", "urn:oasis:names:tc:SAML:2.0:assertion"); + xmlWriter.WriteString(Issuer); + xmlWriter.WriteEndElement(); - xmlWriter.WriteStartElement("saml", "Status", "urn:oasis:names:tc:SAML:2.0:assertion"); - xmlWriter.WriteString(_status); - xmlWriter.WriteEndElement(); + xmlWriter.WriteStartElement("saml", "Status", "urn:oasis:names:tc:SAML:2.0:assertion"); + xmlWriter.WriteString(_status); + xmlWriter.WriteEndElement(); - xmlWriter.WriteEndElement(); + xmlWriter.WriteEndElement(); + } return stringWriter.ToString(); } diff --git a/src/RequestBase.cs b/src/RequestBase.cs index 19865f2..58315c8 100644 --- a/src/RequestBase.cs +++ b/src/RequestBase.cs @@ -4,11 +4,14 @@ using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text; +using System.Xml; +using CoreSaml2Utils.Utilities; namespace CoreSaml2Utils { public abstract class RequestBase { + protected readonly string Id; protected readonly string Issuer; protected readonly string RequestDestination; private readonly X509Certificate2 _cert; @@ -22,6 +25,7 @@ protected RequestBase( Issuer = issuer; RequestDestination = requestDestination; _cert = cert; + Id = $"_{Guid.NewGuid()}"; } //returns the URL you should redirect your users to (i.e. your SAML-provider login URL with the Base64-ed request in the querystring @@ -59,6 +63,25 @@ public string GetRedirectUrl(string samlEndpoint, string relayState, bool sign) return $"{samlEndpoint}{queryStringSeparator}{urlParams}"; } + public string BuildRequestBody(bool sign) + { + var xml = BuildRequestXml(); + + var xmlDocument = new XmlDocument(); + xmlDocument.LoadXml(xml); + + if (sign) + { + var signedXml = SigningHelper.SignXml(xmlDocument, _cert, "ID", Id); + xmlDocument.DocumentElement?.InsertBefore( + signedXml.GetXml(), + xmlDocument.DocumentElement.ChildNodes[0] + ); + } + + return xmlDocument.OuterXml; + } + protected abstract string BuildRequestXml(); protected static string BuildIssueInstant() diff --git a/src/Utilities/CertificateUtilities.cs b/src/Utilities/CertificateUtilities.cs index 5819ba0..30e4f4d 100644 --- a/src/Utilities/CertificateUtilities.cs +++ b/src/Utilities/CertificateUtilities.cs @@ -5,19 +5,13 @@ namespace CoreSaml2Utils.Utilities public static class CertificateUtilities { public static X509Certificate2 LoadCertificateFile(string certificateFilePath, string password = null) - { - return new X509Certificate2(certificateFilePath, password, X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable); - } + => new(certificateFilePath, password, X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable); public static X509Certificate2 LoadCertificate(string certificate) - { - return LoadCertificate(StringToByteArray(certificate)); - } + => LoadCertificate(StringToByteArray(certificate)); public static X509Certificate2 LoadCertificate(byte[] certificate, string password = null) - { - return new X509Certificate2(certificate, password, X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable); - } + => new(certificate, password, X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable); private static byte[] StringToByteArray(string st) { @@ -26,6 +20,7 @@ private static byte[] StringToByteArray(string st) { bytes[i] = (byte)st[i]; } + return bytes; } } diff --git a/src/Utilities/SamlSignedXml.cs b/src/Utilities/SamlSignedXml.cs new file mode 100644 index 0000000..73a8a1d --- /dev/null +++ b/src/Utilities/SamlSignedXml.cs @@ -0,0 +1,25 @@ +using System.Security.Cryptography.Xml; +using System.Xml; + +namespace CoreSaml2Utils.Utilities; + +/// +/// https://github.com/optiklab/SAML-integration-utilities/blob/main/src/SamlIntegration.Utilities/Helpers/SamlSignedXml.cs +/// +internal class SamlSignedXml : SignedXml +{ + private readonly string _referenceAttributeId; + + public SamlSignedXml(XmlDocument document, string referenceAttributeId) : base(document) + { + _referenceAttributeId = referenceAttributeId; + } + + public SamlSignedXml(XmlElement element, string referenceAttributeId) : base(element) + { + _referenceAttributeId = referenceAttributeId; + } + + public override XmlElement GetIdElement(XmlDocument document, string idValue) + => (XmlElement)document.SelectSingleNode($"//*[@{_referenceAttributeId}='{idValue}']"); +} \ No newline at end of file diff --git a/src/Utilities/SigningHelper.cs b/src/Utilities/SigningHelper.cs new file mode 100644 index 0000000..b002c4e --- /dev/null +++ b/src/Utilities/SigningHelper.cs @@ -0,0 +1,52 @@ +using System.Security.Cryptography.X509Certificates; +using System.Security.Cryptography.Xml; +using System.Xml; + +namespace CoreSaml2Utils.Utilities; + +/// +/// https://github.com/optiklab/SAML-integration-utilities/blob/main/src/SamlIntegration.Utilities/Helpers/SigningHelper.cs#L8-L68 +/// +internal class SigningHelper +{ + internal static SamlSignedXml SignXml(XmlDocument doc, X509Certificate2 certificate, string referenceId, string referenceValue) + { + var samlSignedXml = new SamlSignedXml(doc, referenceId); + return SignXml(samlSignedXml, certificate, referenceValue); + } + + internal static SamlSignedXml SignXml(XmlElement element, X509Certificate2 certificate, string referenceId, string referenceValue) + { + var samlSignedXml = new SamlSignedXml(element, referenceId); + return SignXml(samlSignedXml, certificate, referenceValue); + } + + private static SamlSignedXml SignXml(SamlSignedXml samlSignedXml, X509Certificate2 certificate, string referenceValue) + { + samlSignedXml.SigningKey = certificate.PrivateKey; + samlSignedXml.SignedInfo.CanonicalizationMethod = SamlSignedXml.XmlDsigExcC14NTransformUrl; + + // Create a reference to be signed. + var reference = new Reference + { + Uri = "#" + referenceValue + }; + + reference.AddTransform(new XmlDsigEnvelopedSignatureTransform()); + reference.AddTransform(new XmlDsigExcC14NTransform()); + + // Add the reference to the SignedXml object. + samlSignedXml.AddReference(reference); + + // Add an RSAKeyValue KeyInfo (optional; helps recipient find key to validate). + var keyInfo = new KeyInfo(); + keyInfo.AddClause(new KeyInfoX509Data(certificate)); + + samlSignedXml.KeyInfo = keyInfo; + + // Compute the signature. + samlSignedXml.ComputeSignature(); + + return samlSignedXml; + } +} \ No newline at end of file