/// <summary> /// Gets the identity provider certficate. /// </summary> /// <param name="options">The options.</param> /// <param name="serialNumber">The serial number.</param> /// <returns></returns> private X509Certificate2 GetIdentityProviderCertficate(Saml2Options options, string serialNumber) { X509Certificate2 cert = options.Configuration.X509Certificate2.Where(c => c.SerialNumber == serialNumber).FirstOrDefault(); if (cert == null) { throw new Exception("No matching certificate found. Assertion is not from known Idp."); } return(cert); }
/// <summary> /// Validates the X509 certificate signature. /// </summary> /// <param name="xmlDoc">The XML document.</param> /// <param name="options">The options.</param> /// <returns></returns> public bool ValidateX509CertificateSignature(XmlDocument xmlDoc, Saml2Options options) { XmlNodeList XMLSignatures = xmlDoc.GetElementsByTagName(Saml2Constants.Parameters.Signature, Saml2Constants.Namespaces.DsNamespace); if ((options.RequireMessageSigned || options.WantAssertionsSigned) && XMLSignatures.Count == 0) { return(false); } var signedXmlDoc = new SignedXml(xmlDoc); signedXmlDoc.LoadXml((XmlElement)XMLSignatures[0]); //IDP might have multiple signing certs. Get the correct one and check it. KeyInfoX509Data x509data = signedXmlDoc.Signature.KeyInfo.OfType <KeyInfoX509Data>().First(); X509Certificate2 cert = (X509Certificate2)x509data.Certificates[0]; string serialNumber = cert.SerialNumber; var k = signedXmlDoc.Signature; return(signedXmlDoc.CheckSignature(GetIdentityProviderCertficate(options, serialNumber), options.VerifySignatureOnly)); }
/// <summary> /// Creates the authn request. /// </summary> /// <param name="options">The options.</param> /// <param name="authnRequestId">The authn request identifier.</param> /// <param name="relayState">State of the relay.</param> /// <param name="assertionConsumerServiceUrl">The assertion consumer service URL.</param> /// <returns></returns> /// <exception cref="ArgumentException">Signing key must be an instance of either RSA or DSA.</exception> public string CreateAuthnRequest(Saml2Options options, string authnRequestId, string relayState, string assertionConsumerServiceUrl) { NameIDType entityID = new NameIDType() { Value = options.ServiceProvider.EntityId }; var singleSignOnService = options.Configuration.SingleSignOnServices.FirstOrDefault(x => x.Binding == options.AssertionConsumerServiceProtocolBinding); X509Certificate2 spCertificate = GetServiceProviderCertficate(options); AuthnRequest authnRequest = new AuthnRequest() { ID = authnRequestId, Issuer = entityID, Version = Saml2Constants.Version, ForceAuthn = options.ForceAuthn, ForceAuthnSpecified = true, IsPassive = options.IsPassive, IsPassiveSpecified = true, NameIDPolicy = new NameIDPolicyType() { Format = options.NameIDType.Format, SPNameQualifier = options.NameIDType.SPNameQualifier, AllowCreate = true, AllowCreateSpecified = true }, Destination = singleSignOnService.Location.ToString(), ProtocolBinding = singleSignOnService.Binding.ToString(), IssueInstant = DateTime.UtcNow, AssertionConsumerServiceURL = assertionConsumerServiceUrl }; string singleSignOnUrl = options.Configuration.SingleSignOnServices.FirstOrDefault().Location; //serialize AuthnRequest to xml string string xmlTemplate = string.Empty; XmlSerializer xmlSerializer = new XmlSerializer(typeof(AuthnRequest)); using (MemoryStream memStm = new MemoryStream()) { xmlSerializer.Serialize(memStm, authnRequest); memStm.Position = 0; xmlTemplate = new StreamReader(memStm).ReadToEnd(); } //create xml document from string XmlDocument xmlDoc = new XmlDocument(); xmlDoc.LoadXml(xmlTemplate); xmlDoc.PreserveWhitespace = false; string request = xmlDoc.OuterXml; var result = new StringBuilder(); result.AddMessageParameter(request, null); result.AddRelayState(request, relayState); if (options.ServiceProvider.X509Certificate2 != null && options.WantAuthnRequestsSigned) { AsymmetricAlgorithm spPrivateKey = spCertificate.PrivateKey; string hashingAlgorithm = options.Configuration.Signature.SignedInfo.SignatureMethod; // Check if the key is of a supported type. [SAMLBind] sect. 3.4.4.1 specifies this. if (!(spPrivateKey is RSA || spPrivateKey is DSA || spPrivateKey == null)) { throw new ArgumentException("Signing key must be an instance of either RSA or DSA."); } AddSignature(result, spPrivateKey, hashingAlgorithm, options.ServiceProvider.HashAlgorithm); } return($"{singleSignOnUrl}?{result}"); }
/// <summary> /// Gets the service provider certficate. /// </summary> /// <param name="options">The options.</param> /// <returns></returns> private X509Certificate2 GetServiceProviderCertficate(Saml2Options options) { return(options.ServiceProvider.X509Certificate2); }
/// <summary> /// Gets the assertion. /// </summary> /// <param name="idpSamlResponseToken">The idp saml response token.</param> /// <param name="options">The options.</param> /// <returns></returns> /// <exception cref="Exception">Missing assertion /// or /// Unable to parse the decrypted assertion.</exception> /// <exception cref="System.Exception">Missing assertion /// or /// Unable to parse the decrypted assertion.</exception> public string GetAssertion(ResponseType idpSamlResponseToken, Saml2Options options) { string token; string xmlTemplate; var assertion = idpSamlResponseToken.Items[0]; if (assertion == null) { throw new Exception("Missing assertion"); } //check if its a decrypted assertion if (assertion.GetType() == typeof(EncryptedElementType)) { EncryptedElementType encryptedElement = (EncryptedElementType)assertion; SymmetricAlgorithm sessionKey; if (encryptedElement.EncryptedData.EncryptionMethod != null) { sessionKey = ExtractSessionKey(encryptedElement, options.ServiceProvider.X509Certificate2.PrivateKey); var encryptedXml = new EncryptedXml(); XmlSerializer xmlSerializer = new XmlSerializer(typeof(EncryptedDataType)); using (MemoryStream memStm = new MemoryStream()) { xmlSerializer.Serialize(memStm, encryptedElement.EncryptedData); memStm.Position = 0; xmlTemplate = new StreamReader(memStm).ReadToEnd(); } var doc = new XmlDocument(); doc.PreserveWhitespace = true; doc.LoadXml(xmlTemplate); var t = doc.GetElementsByTagName("EncryptedData"); var encryptedData = new EncryptedData(); encryptedData.LoadXml((XmlElement)t[0]); byte[] plaintext = encryptedXml.DecryptData(encryptedData, sessionKey); token = Encoding.UTF8.GetString(plaintext); return(token); } } else { XmlSerializer xmlSerializer = new XmlSerializer(typeof(AssertionType)); using (MemoryStream memStm = new MemoryStream()) { xmlSerializer.Serialize(memStm, assertion); memStm.Position = 0; xmlTemplate = new StreamReader(memStm).ReadToEnd(); } var doc = new XmlDocument(); doc.PreserveWhitespace = true; doc.LoadXml(xmlTemplate); string request = doc.OuterXml; return(request); } throw new Exception("Unable to parse the decrypted assertion."); }
/// <summary> /// Gets the saml response token. /// </summary> /// <param name="base64EncodedSamlResponse">The base64 encoded saml response.</param> /// <param name="responseType">Type of the response.</param> /// <param name="options"></param> /// <returns></returns> /// <exception cref="Exception">Response signature is not valid</exception> /// <exception cref="ArgumentException">Cannot verify signature.</exception> public ResponseType GetSamlResponseToken(string base64EncodedSamlResponse, string responseType, Saml2Options options) { var doc = new XmlDocument { XmlResolver = null, PreserveWhitespace = true }; if (base64EncodedSamlResponse.Contains("%")) { base64EncodedSamlResponse = HttpUtility.UrlDecode(base64EncodedSamlResponse); } byte[] bytes = Convert.FromBase64String(base64EncodedSamlResponse); string samlResponse = Encoding.UTF8.GetString(bytes); doc.LoadXml(samlResponse); if (options.RequireMessageSigned) { if (!ValidateX509CertificateSignature(doc, options)) { throw new Exception("Response signature is not valid"); } } ResponseType samlResponseToken; XmlSerializer xmlSerializers = new XmlSerializer(typeof(ResponseType), new XmlRootAttribute { ElementName = responseType, Namespace = Saml2Constants.Namespaces.Protocol, IsNullable = false }); using (XmlReader reader = new XmlNodeReader(doc)) { samlResponseToken = (ResponseType)xmlSerializers.Deserialize(reader); } return(samlResponseToken); }
/// <summary> /// Creates the logout request. /// The "LogoutRequest" message MUST be signed if the HTTP POST or Redirect binding is used. /// Logout request MUST be signed according to http://docs.oasis-open.org/security/saml/v2.0/saml-profiles-2.0-os.pdf Sect 4.4.3.1 /// </summary> /// <param name="options">The options.</param> /// <param name="logoutRequestId">The logout request identifier.</param> /// <param name="sessionIndex">Index of the session.</param> /// <param name="nameId">The name identifier.</param> /// <param name="relayState">State of the relay.</param> /// <param name="sendSignoutTo">The send signout to.</param> /// <returns></returns> /// <exception cref="ArgumentException">Signing key must be an instance of either RSA or DSA.</exception> public string CreateLogoutRequest(Saml2Options options, string logoutRequestId, string sessionIndex, string nameId, string relayState, string sendSignoutTo) { NameIDType entityID = new NameIDType() { Value = options.ServiceProvider.EntityId }; var singleLogoutService = options.Configuration.SingleLogoutServices.FirstOrDefault(x => x.Binding == options.SingleLogoutServiceProtocolBinding); LogoutRequest logoutRequest = new LogoutRequest() { ID = logoutRequestId, Issuer = entityID, Version = Saml2Constants.Version, Reason = Saml2Constants.Reasons.User, SessionIndex = new string[] { sessionIndex }, Destination = singleLogoutService.Location.ToString(), IssueInstant = DateTime.UtcNow, Item = new NameIDType() { Format = options.NameIDType.Format, NameQualifier = options.NameIDType.NameQualifier, SPProvidedID = options.NameIDType.SPProvidedID, SPNameQualifier = options.NameIDType.SPNameQualifier, Value = options.NameIDType.Value } }; string singleLogoutUrl = options.Configuration.SingleLogoutServices.FirstOrDefault().Location; //serialize AuthnRequest to xml string string xmlTemplate = string.Empty; XmlSerializer xmlSerializer = new XmlSerializer(typeof(LogoutRequest)); using (MemoryStream memStm = new MemoryStream()) { xmlSerializer.Serialize(memStm, logoutRequest); memStm.Position = 0; xmlTemplate = new StreamReader(memStm).ReadToEnd(); } //create xml document from string XmlDocument xmlDoc = new XmlDocument(); xmlDoc.LoadXml(xmlTemplate); xmlDoc.PreserveWhitespace = false; string request = xmlDoc.OuterXml; var result = new StringBuilder(); result.AddMessageParameter(request, null); result.AddRelayState(request, relayState); if (options.hasCertificate && options.WantSignoutRequestsSigned) { X509Certificate2 spCertificate = GetServiceProviderCertficate(options); string hashingAlgorithm = options.Configuration.Signature.SignedInfo.SignatureMethod; AsymmetricAlgorithm spPrivateKey = spCertificate.PrivateKey; // Check if the key is of a supported type. [SAMLBind] sect. 3.4.4.1 specifies this. if (!(spPrivateKey is RSA || spPrivateKey is DSA || spPrivateKey == null)) { throw new ArgumentException("Signing key must be an instance of either RSA or DSA."); } AddSignature(result, spPrivateKey, hashingAlgorithm, options.ServiceProvider.HashAlgorithm); } return($"{singleLogoutUrl}?{result}"); }