Sunday, January 27, 2019

SAML Token – With Self Signed Certificate


SAML is nicely explain in the various internet sites and YouTube videos, so my job here is to make you understand with simple example of generating SAML Token with Self Signed Certificate. 

So lets start with realizing the need of SAML which will help you to recollect your previous knowledge on SAML or to fill the empty brains with basic understanding...

So let’s imagine the below system:

Scenario 1















There’s a CRM system consisting of multiple application and an employee of the company wants to connect to one of the app. The CRM has a big database which will validate if the user has logged in with right credentials and if positive will grant access to the application. Simple – Isn’t it?
But here is the problem of different entities -

Problem of company’s admin – Oh! Employee left the company. I have to make sure he has no more access to company’s different applications in the CRM (he might have access to the social media applications). My routine job -
  1.       Go to CRM check what applications user has access to. May be Admin also directly no access to CRM database?
  2.      Set the flag to no access for company’s applications
  3.      Set the flag for some applications to read only in case of role change
  4.      Etc.etc.


Problem of Employee – Damn! I have to remember hell lot of usernames and password. Is someone can really fish my account?

Problem of CRM Admin – Ah! The population is increasing day by day and so my database is. I am tired of maintaining it.



Got An IDEA - Single Sign On or Internet SSO.




Here are different SSO configurations:
    • Kerberos-based.
    • Smart-card-based.
    • Integrated Windows Authentication.
    • Security Assertion Markup Language.
    • Mobile devices as access credentials.

And different ways of implementing it. (Believe me its a never ending ocean...so will see later :P) Now lets talk about SAML and SAML Token.

What is SAML?
➤ Security Assertion Markup Language, its a secure XML based communication mechanism, basically used for communicating identities between organization.

Per Wiki - 
➤ SAML ian open standard for exchanging authentication and authorization data between parties, in particular, between an identity provider and a service provider. As its name implies, SAML is an XML-based markup language for security assertions.

HOW SAML helps further?

1. Security - saving one from identity theft, fishing by eliminating the number of times user need to login over the internet with different credentials for different applications.

2. Access - User has to no longer type in the password. They just have to click on the app link and simply get into application. Interesting??

3. Administrator work - No need of resetting the lost password, no help desk work.

Okay, enough of theory, Just tell me how to generate SAML Token - 

For generating a SAML token you need a certificate from certificate authority (CA) or you may generate a self signed certificate. There are different way of generating a self signed certificate, here I have used power shell command to get one for me.The SAML token I have generated is self signed.

Run power-shell as an administrator and type below command with desired parameters - 

New-SelfSignedCertificate -Type Custom -Provider "Microsoft RSA SChannel Cryptographic Provider" -Subject "CN=CSNB0466.ap.mydomain.local" -KeyExportPolicy Exportable -KeySpec KeyExchange  -KeyUsage DigitalSignature -KeyAlgorithm RSA -HashAlgorithm sha256 -KeyLength 4096 -CertStoreLocation "Cert:\LocalMachine\My" -FriendlyName "X509_Latest"


to specify more parameters.


This will generate the self signed certificate in store (Run -> MMC -> Add snap ins -> certificate -> Computer Account -> Local Compuyter -> ok). 



Once you get the certificate, you are all set to generate a SAML token and signed it.

So here is my Dot Net Code -

Note - Thumbprint in the certificate can uniquely identify the certificate or its the combination of serial number and issuer distinguish it.

So lets say you got your serial and issuer and some other parameters to pass it an input - 

                CertificateIssuer = "CSNB0466.ap.mydomain.local",
                CertificateSerial = "13cabac0deffd69146f5fe0c1863f7de",
                ServiceProviderNameQualifier = "http://localhost:8080/Portal/Local/",
                SubjectId = "2299091d61e8d9e43cc1e26abf7fe769f1991f8e",
                SubjectNameQualifer = "https://idp.fpehealth.rzv.de/idp/shibboleth",
                SubjectRole = "Physician",
                TokenRecipientUrl = "http://localhost:8080/Portal/"

internal string GetSAMLToken(SAMLTokenRequestDTO sAMLTokenRequest)
{
var organisationVal = "urn:oid:" + GetDBParameterValue("SYSTEM_OBJECT_IDENTIFIER", string.Empty);
var issuerAddress = "http://" + Dns.GetHostName() + "/" + IPGlobalProperties.GetIPGlobalProperties().DomainName + "/idp";
#region Create SAML Assertion Object
Saml2NameIdentifier nameIdentifier = new Saml2NameIdentifier(issuerAddress);
var gc = "_" + Guid.NewGuid().ToString();
Saml2Assertion assertion = new Saml2Assertion(nameIdentifier)
{
Id = new Saml2Id(gc.ToString()),
Issuer = new Saml2NameIdentifier(issuerAddress)
};
#endregion
#region Get Signed Certificate from store
X509Store store = new X509Store(StoreName.My, StoreLocation.LocalMachine);
store.Open(OpenFlags.ReadOnly);
object _certificateIssurer = sAMLTokenRequest.CertificateIssuer;
X509Certificate2Collection certificate2Collection = store.Certificates.Find(X509FindType.FindByIssuerName, _certificateIssurer, false);
X509Certificate2Collection certificate = null;
if (certificate2Collection.Count > 1)
{
certificate = certificate2Collection.Find(X509FindType.FindBySerialNumber, sAMLTokenRequest.CertificateSerial, false);
if (certificate.Count < 1) //within this collection there should be unique serial number
throw new ArgumentException(String.Format("Unable to locate certificate for {0} and {1}", sAMLTokenRequest.CertificateIssuer, sAMLTokenRequest.CertificateSerial));
}
else if (certificate2Collection.Count == 1)
certificate = certificate2Collection;
else
throw new ArgumentException(String.Format("Unable to locate certificate for {0} and {1}", sAMLTokenRequest.CertificateIssuer, sAMLTokenRequest.CertificateSerial));
X509Certificate2 signingCert = certificate[0];
store.Close();
#endregion
#region Add Subject, Condition, Statement, Attributes
AddAssertionSubject(assertion, sAMLTokenRequest.CertificateIssuer,sAMLTokenRequest.SubjectNameQualifer, sAMLTokenRequest.ServiceProviderNameQualifier, sAMLTokenRequest.TokenRecipientUrl);
AddAssertionCondition(assertion, sAMLTokenRequest.TokenRecipientUrl);
AddAuthnStatement(assertion);
AddAttributeStatement(assertion, sAMLTokenRequest.CertificateIssuer, sAMLTokenRequest.SubjectRole, organisationVal);
#endregion
Saml2SecurityToken securityToken = new Saml2SecurityToken(assertion);
Saml2SecurityTokenHandler handler = new Saml2SecurityTokenHandler();
var sb = new StringBuilder();
handler.WriteToken(new XmlPrefixTextWriter(new StringWriter(sb), "saml2"), securityToken);
return SignXmlWithCertificate(sb.ToString(), signingCert, assertion);
}
private static string SignXmlWithCertificate(string assert, X509Certificate2 x509Certificate2, Saml2Assertion assertion)
{
XmlDsigDocument doc = new XmlDsigDocument();
doc.LoadXml(assert);
SignedXml signedXml = new SignedXml(doc);
using (x509Certificate2.GetRSAPrivateKey()) { }
signedXml.SigningKey = x509Certificate2.PrivateKey;
signedXml.SignedInfo.CanonicalizationMethod = SignedXml.XmlDsigExcC14NTransformUrl;
Reference reference = new Reference()
{
Uri = "#" + assertion.Id
};
reference.AddTransform(new XmlDsigEnvelopedSignatureTransform());
reference.AddTransform(new XmlDsigExcC14NTransform());
signedXml.AddReference(reference);
KeyInfo keyInfo = new KeyInfo();
keyInfo.AddClause(new KeyInfoX509Data(x509Certificate2));
signedXml.KeyInfo = keyInfo;
signedXml.ComputeSignature();
XmlElement xmlsig = signedXml.GetXml();
doc.DocumentElement.AppendChild(doc.ImportNode(xmlsig, true));
var xmlstring = new XmlPrefixTextWriter(new StringWriter(new StringBuilder()), "ds:");
return doc.InnerXml;
}
//this is some Organization ID I am fetching from Database.
internal virtual string GetDBParameterValue(string parameterName, string hospitalUnit)
{
string isusValue = string.Empty;
if (parameterName != string.Empty)
{
IConfigurationManager baseSettings = GetConfigurationManager<IConfigurationManager>();
if (hospitalUnit.IsNotNullOrEmpty())
isusValue = baseSettings.GetHospitalSettings(parameterName, hospitalUnit);
else
isusValue = baseSettings.GetHospitalSettings(parameterName);
}
return isusValue;
}
private static void AddAttributeStatement(Saml2Assertion assertion, string name, string role, string organizationVal)
{
Saml2AttributeStatement attributeStatement = new Saml2AttributeStatement();
Saml2Attribute attributeXSPASubject = new Saml2Attribute("urn:oasis:names:tc:xacml:1.0:subject:subject-id")
{
FriendlyName = "XSPA Subject",
Name = "urn:oasis:names:tc:xacml:1.0:subject:subject-id",
NameFormat = new Uri("urn:oasis:names:tc:SAML:2.0:attrname-format:uri"),
};
attributeXSPASubject.Values.Add(name);
attributeStatement.Attributes.Add(attributeXSPASubject);
Saml2Attribute attributeXSPARole = new Saml2Attribute("urn:oasis:names:tc:xacml:2.0:subject:role")
{
FriendlyName = "XSPA Role",
Name = "urn:oasis:names:tc:xacml:2.0:subject:role",
NameFormat = new Uri("urn:oasis:names:tc:SAML:2.0:attrname-format:uri"),
};
attributeXSPARole.Values.Add(role);
attributeStatement.Attributes.Add(attributeXSPARole);
Saml2Attribute attributeXSPAOrganization = new Saml2Attribute("urn:oasis:names:tc:xspa:1.0:subject:organization")
{
FriendlyName = "XSPA Organization",
Name = "urn:oasis:names:tc:xspa:1.0:subject:organization",
NameFormat = new Uri("urn:oasis:names:tc:SAML:2.0:attrname-format:uri"),
};
attributeXSPAOrganization.Values.Add(organizationVal);
attributeStatement.Attributes.Add(attributeXSPAOrganization);
Saml2Attribute attributeXSPAOrganizationId = new Saml2Attribute("urn:oasis:names:tc:xspa:1.0:subject:organization-id")
{
FriendlyName = "XSPA Organization Id",
Name = "urn:oasis:names:tc:xspa:1.0:subject:organization-id",
NameFormat = new Uri("urn:oasis:names:tc:SAML:2.0:attrname-format:uri"),
};
attributeXSPAOrganizationId.Values.Add(organizationVal);
attributeStatement.Attributes.Add(attributeXSPAOrganizationId);
assertion.Statements.Add(attributeStatement);
}
private static void AddAuthnStatement(Saml2Assertion assertion)
{
Saml2AuthenticationContext authenticationContext = new Saml2AuthenticationContext(new Uri("urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport"));
Saml2AuthenticationStatement authenticationStatement = new Saml2AuthenticationStatement(authenticationContext)
{
AuthenticationInstant = DateTime.UtcNow,
SessionIndex = assertion.Id.Value
};
authenticationStatement.SubjectLocality = new Saml2SubjectLocality() { Address = Dns.GetHostEntry(Dns.GetHostName()).AddressList[1].ToString() };
assertion.Statements.Add(authenticationStatement);
}
private static void AddAssertionCondition(Saml2Assertion assertion, string recepientAudience)
{
Saml2Conditions conditions = new Saml2Conditions()
{
NotBefore = DateTime.UtcNow,
NotOnOrAfter = DateTime.UtcNow.AddMinutes(5),
};
Saml2AudienceRestriction audienceRestriction = new Saml2AudienceRestriction(new Uri(recepientAudience));
conditions.AudienceRestrictions.Add(audienceRestriction);
assertion.Conditions = conditions;
}
private static void AddAssertionSubject(Saml2Assertion assertion, string issuerName, string nameQualifier, string spNameQualifier, string recepient)
{
Saml2SubjectConfirmationData subjectConfirmationData = new Saml2SubjectConfirmationData()
{
Address = Dns.GetHostEntry(Dns.GetHostName()).AddressList[1].ToString(),
NotOnOrAfter = DateTime.UtcNow,
Recipient = new Uri(recepient)
};
Saml2SubjectConfirmation subjectConfirmation = new Saml2SubjectConfirmation(new Uri("urn:oasis:names:tc:SAML:2.0:cm:bearer"), subjectConfirmationData);
Saml2NameIdentifier nameIdentifierSubject = new Saml2NameIdentifier(issuerName)
{
Format = new Uri("urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"),
NameQualifier = nameQualifier, //specifies the security or administrative domain that qualifies the name.
SPNameQualifier = spNameQualifier //specifies the name of a service provider or affiliation of providers that is used to further qualify a name
};
Saml2Subject subject = new Saml2Subject(subjectConfirmation)
{
NameId = nameIdentifierSubject
};
assertion.Subject = subject;
}
public class XmlPrefixTextWriter : XmlTextWriter
{
public string LocalPrefix { get; set; }
public XmlPrefixTextWriter(TextWriter w, string localPrefix) : base(w)
{
this.LocalPrefix = localPrefix;
}
public override void WriteStartElement(string prefix, string localName, string ns)
{
base.WriteStartElement(LocalPrefix, localName, ns);
}
}
internal class XmlDsigDocument : XmlDocument
{
/// <summary>
/// Override CreateElement function as it is extensively used by SignedXml
/// </summary>
/// <param name="prefix"></param>
/// <param name="localName"></param>
/// <param name="namespaceURI"></param>
/// <returns></returns>
public override XmlElement CreateElement(string prefix, string localName, string namespaceURI)
{
// uncomment code if "ds" prefix within 'SignedInfo' and descendants is causing siganture to fail.
if (string.IsNullOrEmpty(prefix))
{
// List<string> SignedInfoAndDescendants = new List<string>();
// SignedInfoAndDescendants.Add("SignedInfo");
// SignedInfoAndDescendants.Add("CanonicalizationMethod");
// SignedInfoAndDescendants.Add("InclusiveNamespaces");
// SignedInfoAndDescendants.Add("SignatureMethod");
// SignedInfoAndDescendants.Add("Reference");
// SignedInfoAndDescendants.Add("Transforms");
// SignedInfoAndDescendants.Add("Transform");
// SignedInfoAndDescendants.Add("InclusiveNamespaces");
// SignedInfoAndDescendants.Add("DigestMethod");
// SignedInfoAndDescendants.Add("DigestValue");
// if (!SignedInfoAndDescendants.Contains(localName))
// {
prefix = GetPrefix(namespaceURI);
}
// }
return base.CreateElement(prefix, localName, namespaceURI);
}
/// <summary>
/// Select the standard prefix for the namespaceURI provided
/// </summary>
/// <param name="namespaceURI"></param>
/// <returns></returns>
private string GetPrefix(string namespaceURI)
{
if (namespaceURI == SignedXml.XmlDsigNamespaceUrl)
return "ds";
else if(namespaceURI == "http://www.w3.org/2001/10/xml-exc-c14n#")
return "ec";
return string.Empty;
}
}
view raw gistfile1.txt hosted with ❤ by GitHub