mirror of
https://github.com/9p4/jellyfin-plugin-sso
synced 2026-10-21 06:08:09 +11:00
Merge branch 'clean' of https://github.com/crobibero/jellyfin-plugin-sso into crobibero-clean
This commit is contained in:
@@ -1,57 +1,59 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Xml.Serialization;
|
||||
|
||||
namespace Jellyfin.Plugin.SSO_Auth.Config {
|
||||
namespace Jellyfin.Plugin.SSO_Auth.Config;
|
||||
|
||||
/// <summary>
|
||||
/// Plugin Configuration.
|
||||
/// </summary>
|
||||
public class PluginConfiguration : MediaBrowser.Model.Plugins.BasePluginConfiguration
|
||||
{
|
||||
/// <summary>
|
||||
/// Plugin Configuration.
|
||||
/// Initializes a new instance of the <see cref="PluginConfiguration"/> class.
|
||||
/// </summary>
|
||||
public class PluginConfiguration : MediaBrowser.Model.Plugins.BasePluginConfiguration {
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PluginConfiguration"/> class.
|
||||
/// </summary>
|
||||
public PluginConfiguration()
|
||||
{
|
||||
SamlConfigs = new List<SamlConfig>();
|
||||
OIDConfigs = new List<OIDConfig>();
|
||||
}
|
||||
|
||||
[XmlArray("SamlConfigs"), XmlArrayItem(typeof(SamlConfig), ElementName = "SamlConfigs")]
|
||||
public List<SamlConfig> SamlConfigs { get; set; }
|
||||
|
||||
[XmlArray("OIDConfigs"), XmlArrayItem(typeof(OIDConfig), ElementName = "OIDConfigs")]
|
||||
public List<OIDConfig> 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<SamlConfig>();
|
||||
OIDConfigs = new List<OIDConfig>();
|
||||
}
|
||||
|
||||
[XmlRoot("PluginConfiguration")]
|
||||
public class OIDConfig
|
||||
{
|
||||
public string OIDEndpoint { get; set; }
|
||||
[XmlArray("SamlConfigs")]
|
||||
[XmlArrayItem(typeof(SamlConfig), ElementName = "SamlConfigs")]
|
||||
public List<SamlConfig> 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<OIDConfig> 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; }
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.376" PrivateAssets="All" />
|
||||
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
+15
-12
@@ -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<PluginConfiguration>, IHasWebPages {
|
||||
public SSOPlugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer) : base(applicationPaths, xmlSerializer) {
|
||||
Instance = this;
|
||||
namespace Jellyfin.Plugin.SSO_Auth;
|
||||
|
||||
public class SSOPlugin : BasePlugin<PluginConfiguration>, 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<PluginPageInfo> GetPages() {
|
||||
return new[] {
|
||||
new PluginPageInfo {
|
||||
Name = Name,
|
||||
EmbeddedResourcePath = $"{GetType().Namespace}.Config.configPage.html"
|
||||
}
|
||||
};
|
||||
public IEnumerable<PluginPageInfo> GetPages()
|
||||
{
|
||||
yield return new PluginPageInfo
|
||||
{
|
||||
Name = Name,
|
||||
EmbeddedResourcePath = $"{GetType().Namespace}.Config.configPage.html"
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+260
-232
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Base64 request.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="samlEndpoint">The SAML endpoint.</param>
|
||||
/// <param name="relayState">The relay state.</param>
|
||||
/// <returns>The redirect url.</returns>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user