Skip to content

Commit

Permalink
testing logout functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
mrtristan committed Jul 25, 2024
1 parent 9efbe03 commit c66d04e
Show file tree
Hide file tree
Showing 10 changed files with 175 additions and 63 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
18 changes: 18 additions & 0 deletions src/AssertionParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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");
Expand Down
35 changes: 18 additions & 17 deletions src/AuthnRequest.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using System;
using System.IO;
using System.Security.Cryptography.X509Certificates;
using System.Xml;
Expand Down Expand Up @@ -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();
}
Expand Down
2 changes: 1 addition & 1 deletion src/CoreSaml2Utils.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
<LangVersion>preview</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.Security.Cryptography.Xml" Version="5.0.0" />
<PackageReference Include="System.Security.Cryptography.Xml" Version="8.0.1" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
Expand Down
34 changes: 18 additions & 16 deletions src/LogoutRequest.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using System;
using System.IO;
using System.Security.Cryptography.X509Certificates;
using System.Xml;
Expand Down Expand Up @@ -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();
}
Expand Down
35 changes: 15 additions & 20 deletions src/LogoutResponse.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using System;
using System.IO;
using System.Security.Cryptography.X509Certificates;
using System.Xml;
Expand All @@ -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
)
{
Expand All @@ -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();
}
Expand Down
23 changes: 23 additions & 0 deletions src/RequestBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -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()
Expand Down
13 changes: 4 additions & 9 deletions src/Utilities/CertificateUtilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand All @@ -26,6 +20,7 @@ private static byte[] StringToByteArray(string st)
{
bytes[i] = (byte)st[i];
}

return bytes;
}
}
Expand Down
25 changes: 25 additions & 0 deletions src/Utilities/SamlSignedXml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System.Security.Cryptography.Xml;
using System.Xml;

namespace CoreSaml2Utils.Utilities;

/// <summary>
/// https://github.com/optiklab/SAML-integration-utilities/blob/main/src/SamlIntegration.Utilities/Helpers/SamlSignedXml.cs
/// </summary>
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}']");
}
52 changes: 52 additions & 0 deletions src/Utilities/SigningHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using System.Security.Cryptography.X509Certificates;
using System.Security.Cryptography.Xml;
using System.Xml;

namespace CoreSaml2Utils.Utilities;

/// <summary>
/// https://github.com/optiklab/SAML-integration-utilities/blob/main/src/SamlIntegration.Utilities/Helpers/SigningHelper.cs#L8-L68
/// </summary>
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;
}
}

0 comments on commit c66d04e

Please sign in to comment.