diff --git a/SSO-Auth/Config/PluginConfiguration.cs b/SSO-Auth/Config/PluginConfiguration.cs index 1f90502..c2c0d09 100644 --- a/SSO-Auth/Config/PluginConfiguration.cs +++ b/SSO-Auth/Config/PluginConfiguration.cs @@ -1,57 +1,59 @@ using System.Collections.Generic; using System.Xml.Serialization; -namespace Jellyfin.Plugin.SSO_Auth.Config { +namespace Jellyfin.Plugin.SSO_Auth.Config; + +/// +/// Plugin Configuration. +/// +public class PluginConfiguration : MediaBrowser.Model.Plugins.BasePluginConfiguration +{ /// - /// Plugin Configuration. + /// Initializes a new instance of the class. /// - public class PluginConfiguration : MediaBrowser.Model.Plugins.BasePluginConfiguration { - /// - /// Initializes a new instance of the class. - /// - public PluginConfiguration() - { - SamlConfigs = new List(); - OIDConfigs = new List(); - } - - [XmlArray("SamlConfigs"), XmlArrayItem(typeof(SamlConfig), ElementName = "SamlConfigs")] - public List SamlConfigs { get; set; } - - [XmlArray("OIDConfigs"), XmlArrayItem(typeof(OIDConfig), ElementName = "OIDConfigs")] - public List OIDConfigs { get; set; } - - } - - [XmlRoot("PluginConfiguration")] - public class SamlConfig + public PluginConfiguration() { - public string SamlEndpoint { get; set; } - - public string SamlClientId { get; set; } - - public string SamlCertificate { get; set; } - - public bool Enabled { get; set; } - - public bool EnableAllFolders { get; set; } - - public string[] EnabledFolders { get; set; } + SamlConfigs = new List(); + OIDConfigs = new List(); } - [XmlRoot("PluginConfiguration")] - public class OIDConfig - { - public string OIDEndpoint { get; set; } + [XmlArray("SamlConfigs")] + [XmlArrayItem(typeof(SamlConfig), ElementName = "SamlConfigs")] + public List SamlConfigs { get; set; } - public string OIDClientId { get; set; } - - public string OIDSecret { get; set; } - - public bool Enabled { get; set; } - - public bool EnableAllFolders { get; set; } - - public string[] EnabledFolders { get; set; } - } + [XmlArray("OIDConfigs")] + [XmlArrayItem(typeof(OIDConfig), ElementName = "OIDConfigs")] + public List OIDConfigs { get; set; } +} + +[XmlRoot("PluginConfiguration")] +public class SamlConfig +{ + public string SamlEndpoint { get; set; } + + public string SamlClientId { get; set; } + + public string SamlCertificate { get; set; } + + public bool Enabled { get; set; } + + public bool EnableAllFolders { get; set; } + + public string[] EnabledFolders { get; set; } +} + +[XmlRoot("PluginConfiguration")] +public class OIDConfig +{ + public string OIDEndpoint { get; set; } + + public string OIDClientId { get; set; } + + public string OIDSecret { get; set; } + + public bool Enabled { get; set; } + + public bool EnableAllFolders { get; set; } + + public string[] EnabledFolders { get; set; } } diff --git a/SSO-Auth/SSO-Auth.csproj b/SSO-Auth/SSO-Auth.csproj index c980519..a3b33d1 100644 --- a/SSO-Auth/SSO-Auth.csproj +++ b/SSO-Auth/SSO-Auth.csproj @@ -24,7 +24,7 @@ - + diff --git a/SSO-Auth/SSOPlugin.cs b/SSO-Auth/SSOPlugin.cs index f63ea53..967f574 100644 --- a/SSO-Auth/SSOPlugin.cs +++ b/SSO-Auth/SSOPlugin.cs @@ -6,10 +6,14 @@ using MediaBrowser.Common.Plugins; using MediaBrowser.Model.Plugins; using MediaBrowser.Model.Serialization; -namespace Jellyfin.Plugin.SSO_Auth { - public class SSOPlugin : BasePlugin, IHasWebPages { - public SSOPlugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer) : base(applicationPaths, xmlSerializer) { - Instance = this; +namespace Jellyfin.Plugin.SSO_Auth; + +public class SSOPlugin : BasePlugin, IHasWebPages +{ + public SSOPlugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer) + : base(applicationPaths, xmlSerializer) + { + Instance = this; } public static SSOPlugin Instance { get; private set; } @@ -18,13 +22,12 @@ namespace Jellyfin.Plugin.SSO_Auth { public override Guid Id => Guid.Parse("505ce9d1-d916-42fa-86ca-673ef241d7df"); - public IEnumerable GetPages() { - return new[] { - new PluginPageInfo { - Name = Name, - EmbeddedResourcePath = $"{GetType().Namespace}.Config.configPage.html" - } - }; + public IEnumerable GetPages() + { + yield return new PluginPageInfo + { + Name = Name, + EmbeddedResourcePath = $"{GetType().Namespace}.Config.configPage.html" + }; } - } } diff --git a/SSO-Auth/Saml.cs b/SSO-Auth/Saml.cs index b02ecc6..8e4321d 100644 --- a/SSO-Auth/Saml.cs +++ b/SSO-Auth/Saml.cs @@ -1,273 +1,301 @@ -/* Jitbit's simple SAML 2.0 component for ASP.NET - https://github.com/jitbit/AspNetSaml/ - (c) Jitbit LP, 2016 - Use this freely under the Apache license (see https://choosealicense.com/licenses/apache-2.0/) - version 1.2.3 +/* + Jitbit's simple SAML 2.0 component for ASP.NET + https://github.com/jitbit/AspNetSaml/ + (c) Jitbit LP, 2016 + Use this freely under the Apache license (see https://choosealicense.com/licenses/apache-2.0/) + version 1.2.3 */ using System; -using System.Web; using System.IO; -using System.Xml; +using System.IO.Compression; using System.Security.Cryptography.X509Certificates; using System.Security.Cryptography.Xml; -using System.IO.Compression; using System.Text; +using System.Web; +using System.Xml; -namespace Saml +namespace Jellyfin.Plugin.SSO_Auth; + +public class Response { - public partial class Response - { - protected XmlDocument _xmlDoc; - protected readonly X509Certificate2 _certificate; - protected XmlNamespaceManager _xmlNameSpaceManager; //we need this one to run our XPath queries on the SAML XML + private readonly X509Certificate2 _certificate; + private XmlDocument _xmlDoc; + private XmlNamespaceManager _xmlNameSpaceManager; // we need this one to run our XPath queries on the SAML XML - public string Xml { get { return _xmlDoc.OuterXml; } } + public Response(string certificateStr, string responseString) + : this(Convert.FromBase64String(certificateStr), responseString) + { + } - public Response(string certificateStr, string responseString) - : this(Convert.FromBase64String(certificateStr), responseString) { } + public Response(byte[] certificateBytes, string responseString) : this(certificateBytes) + { + LoadXmlFromBase64(responseString); + } - public Response(byte[] certificateBytes, string responseString) : this(certificateBytes) - { - LoadXmlFromBase64(responseString); - } + public Response(string certificateStr) : this(Convert.FromBase64String(certificateStr)) + { + } - public Response(string certificateStr) : this(Convert.FromBase64String(certificateStr)) { } + public Response(byte[] certificateBytes) + { + _certificate = new X509Certificate2(certificateBytes); + } - public Response(byte[] certificateBytes) - { - _certificate = new X509Certificate2(certificateBytes); - } + public string Xml => _xmlDoc.OuterXml; - public void LoadXml(string xml) - { - _xmlDoc = new XmlDocument(); - _xmlDoc.PreserveWhitespace = true; - _xmlDoc.XmlResolver = null; - _xmlDoc.LoadXml(xml); + public void LoadXml(string xml) + { + _xmlDoc = new XmlDocument(); + _xmlDoc.PreserveWhitespace = true; + _xmlDoc.XmlResolver = null; + _xmlDoc.LoadXml(xml); - _xmlNameSpaceManager = GetNamespaceManager(); //lets construct a "manager" for XPath queries - } + _xmlNameSpaceManager = GetNamespaceManager(); // lets construct a "manager" for XPath queries + } - public void LoadXmlFromBase64(string response) - { - LoadXml(System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(response))); - } + public void LoadXmlFromBase64(string response) + { + LoadXml(Encoding.UTF8.GetString(Convert.FromBase64String(response))); + } - public bool IsValid() - { - XmlNodeList nodeList = _xmlDoc.SelectNodes("//ds:Signature", _xmlNameSpaceManager); + public bool IsValid() + { + var nodeList = _xmlDoc.SelectNodes("//ds:Signature", _xmlNameSpaceManager); - SignedXml signedXml = new SignedXml(_xmlDoc); + var signedXml = new SignedXml(_xmlDoc); - if (nodeList.Count == 0) return false; + if (nodeList.Count == 0) + { + return false; + } - signedXml.LoadXml((XmlElement)nodeList[0]); - return ValidateSignatureReference(signedXml) && signedXml.CheckSignature(_certificate, true) && !IsExpired(); - } + signedXml.LoadXml((XmlElement)nodeList[0]); + return ValidateSignatureReference(signedXml) && signedXml.CheckSignature(_certificate, true) && !IsExpired(); + } - //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 - private bool ValidateSignatureReference(SignedXml signedXml) - { - if (signedXml.SignedInfo.References.Count != 1) //no ref at all - return false; + // 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 + private bool ValidateSignatureReference(SignedXml signedXml) + { + if (signedXml.SignedInfo.References.Count != 1) // no ref at all + { + return false; + } - var reference = (Reference)signedXml.SignedInfo.References[0]; - var id = reference.Uri.Substring(1); + var reference = (Reference)signedXml.SignedInfo.References[0]; + var id = reference.Uri.Substring(1); - var idElement = signedXml.GetIdElement(_xmlDoc, id); + var idElement = signedXml.GetIdElement(_xmlDoc, id); - if (idElement == _xmlDoc.DocumentElement) - 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; - if (assertionNode != idElement) - return false; - } + if (idElement == _xmlDoc.DocumentElement) + { + 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; + if (assertionNode != idElement) + { + return false; + } + } - return true; - } + return true; + } - private 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 DateTime.UtcNow > expirationDate.ToUniversalTime(); - } + private bool IsExpired() + { + var expirationDate = DateTime.MaxValue; + var 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); + } - public string GetNameID() - { - XmlNode node = _xmlDoc.SelectSingleNode("/samlp:Response/saml:Assertion[1]/saml:Subject/saml:NameID", _xmlNameSpaceManager); - return node.InnerText; - } + return DateTime.UtcNow > expirationDate.ToUniversalTime(); + } - public virtual string GetUpn() - { - return GetCustomAttribute("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn"); - } + public string GetNameID() + { + var node = _xmlDoc.SelectSingleNode("/samlp:Response/saml:Assertion[1]/saml:Subject/saml:NameID", _xmlNameSpaceManager); + return node.InnerText; + } - public virtual string GetEmail() - { - return GetCustomAttribute("User.email") - ?? GetCustomAttribute("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress") //some providers (for example Azure AD) put last name into an attribute named "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress" - ?? GetCustomAttribute("mail"); //some providers put last name into an attribute named "mail" - } + public virtual string GetUpn() + { + return GetCustomAttribute("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn"); + } - public virtual string GetFirstName() - { - return GetCustomAttribute("first_name") - ?? GetCustomAttribute("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname") //some providers (for example Azure AD) put last name into an attribute named "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname" - ?? GetCustomAttribute("User.FirstName") - ?? GetCustomAttribute("givenName"); //some providers put last name into an attribute named "givenName" - } + public virtual string GetEmail() + { + return GetCustomAttribute("User.email") + // some providers (for example Azure AD) put last name into an attribute named "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress" + ?? GetCustomAttribute("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress") + // some providers put last name into an attribute named "mail" + ?? GetCustomAttribute("mail"); + } - public virtual string GetLastName() - { - return GetCustomAttribute("last_name") - ?? GetCustomAttribute("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname") //some providers (for example Azure AD) put last name into an attribute named "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname" - ?? GetCustomAttribute("User.LastName") - ?? GetCustomAttribute("sn"); //some providers put last name into an attribute named "sn" - } + public virtual string GetFirstName() + { + return GetCustomAttribute("first_name") + // some providers (for example Azure AD) put last name into an attribute named "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname" + ?? GetCustomAttribute("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname") + ?? GetCustomAttribute("User.FirstName") + // some providers put last name into an attribute named "givenName" + ?? GetCustomAttribute("givenName"); + } - public virtual string GetDepartment() - { - return GetCustomAttribute("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/department") - ?? GetCustomAttribute("department"); - } + public virtual string GetLastName() + { + return GetCustomAttribute("last_name") + // some providers (for example Azure AD) put last name into an attribute named "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname" + ?? GetCustomAttribute("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname") + ?? GetCustomAttribute("User.LastName") + // some providers put last name into an attribute named "sn" + ?? GetCustomAttribute("sn"); + } - public virtual string GetPhone() - { - return GetCustomAttribute("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/homephone") - ?? GetCustomAttribute("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/telephonenumber"); - } + public virtual string GetDepartment() + { + return GetCustomAttribute("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/department") + ?? GetCustomAttribute("department"); + } - public virtual string GetCompany() - { - return GetCustomAttribute("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/companyname") - ?? GetCustomAttribute("organization") - ?? GetCustomAttribute("User.CompanyName"); - } + public virtual string GetPhone() + { + return GetCustomAttribute("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/homephone") + ?? GetCustomAttribute("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/telephonenumber"); + } - public virtual string GetLocation() - { - return GetCustomAttribute("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/location") - ?? GetCustomAttribute("physicalDeliveryOfficeName"); - } + public virtual string GetCompany() + { + return GetCustomAttribute("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/companyname") + ?? GetCustomAttribute("organization") + ?? GetCustomAttribute("User.CompanyName"); + } - public string GetCustomAttribute(string attr) - { - XmlNode node = _xmlDoc.SelectSingleNode("/samlp:Response/saml:Assertion[1]/saml:AttributeStatement/saml:Attribute[@Name='" + attr + "']/saml:AttributeValue", _xmlNameSpaceManager); - return node == null ? null : node.InnerText; - } + public virtual string GetLocation() + { + return GetCustomAttribute("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/location") + ?? GetCustomAttribute("physicalDeliveryOfficeName"); + } - //returns namespace manager, we need one b/c MS says so... Otherwise XPath doesnt work in an XML doc with namespaces - //see https://stackoverflow.com/questions/7178111/why-is-xmlnamespacemanager-necessary - private XmlNamespaceManager GetNamespaceManager() - { - XmlNamespaceManager manager = new XmlNamespaceManager(_xmlDoc.NameTable); - manager.AddNamespace("ds", SignedXml.XmlDsigNamespaceUrl); - manager.AddNamespace("saml", "urn:oasis:names:tc:SAML:2.0:assertion"); - manager.AddNamespace("samlp", "urn:oasis:names:tc:SAML:2.0:protocol"); + public string GetCustomAttribute(string attr) + { + var node = _xmlDoc.SelectSingleNode("/samlp:Response/saml:Assertion[1]/saml:AttributeStatement/saml:Attribute[@Name='" + attr + "']/saml:AttributeValue", _xmlNameSpaceManager); + return node?.InnerText; + } - return manager; - } - } + // returns namespace manager, we need one b/c MS says so... Otherwise XPath doesnt work in an XML doc with namespaces + // see https://stackoverflow.com/questions/7178111/why-is-xmlnamespacemanager-necessary + private XmlNamespaceManager GetNamespaceManager() + { + var manager = new XmlNamespaceManager(_xmlDoc.NameTable); + manager.AddNamespace("ds", SignedXml.XmlDsigNamespaceUrl); + manager.AddNamespace("saml", "urn:oasis:names:tc:SAML:2.0:assertion"); + manager.AddNamespace("samlp", "urn:oasis:names:tc:SAML:2.0:protocol"); - public class AuthRequest - { - public string _id; - private string _issue_instant; - - private string _issuer; - private string _assertionConsumerServiceUrl; - - public enum AuthRequestFormat - { - Base64 = 1 - } - - public AuthRequest(string issuer, string assertionConsumerServiceUrl) - { - _id = "_" + Guid.NewGuid().ToString(); - _issue_instant = DateTime.Now.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ", System.Globalization.CultureInfo.InvariantCulture); - - _issuer = issuer; - _assertionConsumerServiceUrl = assertionConsumerServiceUrl; - } - - public string GetRequest(AuthRequestFormat format) - { - using (StringWriter sw = new StringWriter()) - { - XmlWriterSettings xws = new XmlWriterSettings(); - xws.OmitXmlDeclaration = true; - - using (XmlWriter xw = XmlWriter.Create(sw, xws)) - { - xw.WriteStartElement("samlp", "AuthnRequest", "urn:oasis:names:tc:SAML:2.0:protocol"); - xw.WriteAttributeString("ID", _id); - xw.WriteAttributeString("Version", "2.0"); - xw.WriteAttributeString("IssueInstant", _issue_instant); - xw.WriteAttributeString("ProtocolBinding", "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"); - xw.WriteAttributeString("AssertionConsumerServiceURL", _assertionConsumerServiceUrl); - - xw.WriteStartElement("saml", "Issuer", "urn:oasis:names:tc:SAML:2.0:assertion"); - xw.WriteString(_issuer); - xw.WriteEndElement(); - - xw.WriteStartElement("samlp", "NameIDPolicy", "urn:oasis:names:tc:SAML:2.0:protocol"); - xw.WriteAttributeString("Format", "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"); - xw.WriteAttributeString("AllowCreate", "true"); - xw.WriteEndElement(); - - /*xw.WriteStartElement("samlp", "RequestedAuthnContext", "urn:oasis:names:tc:SAML:2.0:protocol"); - xw.WriteAttributeString("Comparison", "exact"); - xw.WriteStartElement("saml", "AuthnContextClassRef", "urn:oasis:names:tc:SAML:2.0:assertion"); - xw.WriteString("urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport"); - xw.WriteEndElement(); - xw.WriteEndElement();*/ - - xw.WriteEndElement(); - } - - if (format == AuthRequestFormat.Base64) - { - //byte[] toEncodeAsBytes = System.Text.ASCIIEncoding.ASCII.GetBytes(sw.ToString()); - //return System.Convert.ToBase64String(toEncodeAsBytes); - - //https://stackoverflow.com/questions/25120025/acs75005-the-request-is-not-a-valid-saml2-protocol-message-is-showing-always%3C/a%3E - var memoryStream = new MemoryStream(); - var writer = new StreamWriter(new DeflateStream(memoryStream, CompressionMode.Compress, true), new UTF8Encoding(false)); - writer.Write(sw.ToString()); - writer.Close(); - string result = Convert.ToBase64String(memoryStream.GetBuffer(), 0, (int)memoryStream.Length, Base64FormattingOptions.None); - return result; - } - - return null; - } - } - - //returns the URL you should redirect your users to (i.e. your SAML-provider login URL with the Base64-ed request in the querystring - public string GetRedirectUrl(string samlEndpoint, string relayState = null) - { - var queryStringSeparator = samlEndpoint.Contains("?") ? "&" : "?"; - - var url = samlEndpoint + queryStringSeparator + "SAMLRequest=" + HttpUtility.UrlEncode(GetRequest(AuthRequestFormat.Base64)); - - if (!string.IsNullOrEmpty(relayState)) - { - url += "&RelayState=" + HttpUtility.UrlEncode(relayState); - } - - return url; - } - } + return manager; + } +} + +public class AuthRequest +{ + private readonly string _id; + private readonly string _issueInstant; + + private readonly string _issuer; + private readonly string _assertionConsumerServiceUrl; + + public AuthRequest(string issuer, string assertionConsumerServiceUrl) + { + _id = "_" + Guid.NewGuid().ToString(); + _issueInstant = DateTime.Now.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ", System.Globalization.CultureInfo.InvariantCulture); + + _issuer = issuer; + _assertionConsumerServiceUrl = assertionConsumerServiceUrl; + } + + public enum AuthRequestFormat + { + /// + /// Base64 request. + /// + Base64 = 1 + } + + public string GetRequest(AuthRequestFormat format) + { + using var sw = new StringWriter(); + var xws = new XmlWriterSettings(); + xws.OmitXmlDeclaration = true; + + using (var xw = XmlWriter.Create(sw, xws)) + { + xw.WriteStartElement("samlp", "AuthnRequest", "urn:oasis:names:tc:SAML:2.0:protocol"); + xw.WriteAttributeString("ID", _id); + xw.WriteAttributeString("Version", "2.0"); + xw.WriteAttributeString("IssueInstant", _issueInstant); + xw.WriteAttributeString("ProtocolBinding", "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"); + xw.WriteAttributeString("AssertionConsumerServiceURL", _assertionConsumerServiceUrl); + + xw.WriteStartElement("saml", "Issuer", "urn:oasis:names:tc:SAML:2.0:assertion"); + xw.WriteString(_issuer); + xw.WriteEndElement(); + + xw.WriteStartElement("samlp", "NameIDPolicy", "urn:oasis:names:tc:SAML:2.0:protocol"); + xw.WriteAttributeString("Format", "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"); + xw.WriteAttributeString("AllowCreate", "true"); + xw.WriteEndElement(); + + /* + xw.WriteStartElement("samlp", "RequestedAuthnContext", "urn:oasis:names:tc:SAML:2.0:protocol"); + xw.WriteAttributeString("Comparison", "exact"); + xw.WriteStartElement("saml", "AuthnContextClassRef", "urn:oasis:names:tc:SAML:2.0:assertion"); + xw.WriteString("urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport"); + xw.WriteEndElement(); + xw.WriteEndElement(); + */ + + xw.WriteEndElement(); + } + + if (format == AuthRequestFormat.Base64) + { + // byte[] toEncodeAsBytes = System.Text.ASCIIEncoding.ASCII.GetBytes(sw.ToString()); + // return System.Convert.ToBase64String(toEncodeAsBytes); + + // https://stackoverflow.com/questions/25120025/acs75005-the-request-is-not-a-valid-saml2-protocol-message-is-showing-always%3C/a%3E + var memoryStream = new MemoryStream(); + var writer = new StreamWriter(new DeflateStream(memoryStream, CompressionMode.Compress, true), new UTF8Encoding(false)); + writer.Write(sw.ToString()); + writer.Close(); + var result = Convert.ToBase64String(memoryStream.GetBuffer(), 0, (int)memoryStream.Length, Base64FormattingOptions.None); + return result; + } + + return null; + } + + /// + /// Gets the the URL you should redirect your users to (i.e. your SAML-provider login URL with the Base64-ed request in the querystring. + /// + /// The SAML endpoint. + /// The relay state. + /// The redirect url. + public string GetRedirectUrl(string samlEndpoint, string relayState = null) + { + var queryStringSeparator = samlEndpoint.Contains('?') ? "&" : "?"; + + var url = samlEndpoint + queryStringSeparator + "SAMLRequest=" + HttpUtility.UrlEncode(GetRequest(AuthRequestFormat.Base64)); + + if (!string.IsNullOrEmpty(relayState)) + { + url += "&RelayState=" + HttpUtility.UrlEncode(relayState); + } + + return url; + } }