diff --git a/.editorconfig b/.editorconfig
index 7753a3b..2e62bca 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -1,7 +1,11 @@
root = true
[*]
+end_of_line = lf
insert_final_newline = true
+indent_style = tab
+indent_size = 4
+tab_width = 4
[*.cs]
indent_style = tab
@@ -79,7 +83,7 @@ dotnet_naming_style.pascal_case.capitalization = pascal_case
dotnet_style_operator_placement_when_wrapping = beginning_of_line
tab_width = 4
indent_size = 4
-end_of_line = crlf
+end_of_line = lf
dotnet_style_coalesce_expression = true:suggestion
dotnet_style_null_propagation = true:suggestion
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
diff --git a/AspNetSaml.Tests/UnitTests.cs b/AspNetSaml.Tests/UnitTests.cs
index e02fbe5..66290d4 100644
--- a/AspNetSaml.Tests/UnitTests.cs
+++ b/AspNetSaml.Tests/UnitTests.cs
@@ -1,221 +1,221 @@
-using Saml;
-using System.IO.Compression;
-using System.Text;
-using Shouldly;
-using System.Security.Claims;
-
-namespace AspNetSaml.Tests
-{
- [TestClass]
- public class UnitTests
- {
- //cert and signature taken form here: www.samltool.com/generic_sso_res.php
-
- [TestMethod]
- public void TestSamlResponseValidator()
- {
- var cert = @"-----BEGIN CERTIFICATE-----
-MIICajCCAdOgAwIBAgIBADANBgkqhkiG9w0BAQ0FADBSMQswCQYDVQQGEwJ1czETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMT25lbG9naW4gSW5jMRcwFQYDVQQDDA5zcC5leGFtcGxlLmNvbTAeFw0xNDA3MTcxNDEyNTZaFw0xNTA3MTcxNDEyNTZaMFIxCzAJBgNVBAYTAnVzMRMwEQYDVQQIDApDYWxpZm9ybmlhMRUwEwYDVQQKDAxPbmVsb2dpbiBJbmMxFzAVBgNVBAMMDnNwLmV4YW1wbGUuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDZx+ON4IUoIWxgukTb1tOiX3bMYzYQiwWPUNMp+Fq82xoNogso2bykZG0yiJm5o8zv/sd6pGouayMgkx/2FSOdc36T0jGbCHuRSbtia0PEzNIRtmViMrt3AeoWBidRXmZsxCNLwgIV6dn2WpuE5Az0bHgpZnQxTKFek0BMKU/d8wIDAQABo1AwTjAdBgNVHQ4EFgQUGHxYqZYyX7cTxKVODVgZwSTdCnwwHwYDVR0jBBgwFoAUGHxYqZYyX7cTxKVODVgZwSTdCnwwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQ0FAAOBgQByFOl+hMFICbd3DJfnp2Rgd/dqttsZG/tyhILWvErbio/DEe98mXpowhTkC04ENprOyXi7ZbUqiicF89uAGyt1oqgTUCD1VsLahqIcmrzgumNyTwLGWo17WDAa1/usDhetWAMhgzF/Cnf5ek0nK00m0YZGyc4LzgD0CROMASTWNg==
------END CERTIFICATE-----";
-
- var samlresp = new Saml.Response(cert);
- samlresp.LoadXml(@"
-
- http://idp.example.com/metadata.php
-
-
- 99Bke1BpL1yOfGd5ADkGSle2sZg=OOyb3YtYQm3DC7gj6lQPM20r76HH4KvAE93f5xrIuIHGk8ZJlse4m8t4msLkhwUEAGwWOOVyHs8gChtN1m/P4pKCXyttO9Hev14Wz8E1R444kg5Yak+02FZ+Fn3VbbPq+kY4eYRkczNMphivWkdwc/QjDguNzGoKCEEtbBKDMGg=
-MIICajCCAdOgAwIBAgIBADANBgkqhkiG9w0BAQ0FADBSMQswCQYDVQQGEwJ1czETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMT25lbG9naW4gSW5jMRcwFQYDVQQDDA5zcC5leGFtcGxlLmNvbTAeFw0xNDA3MTcxNDEyNTZaFw0xNTA3MTcxNDEyNTZaMFIxCzAJBgNVBAYTAnVzMRMwEQYDVQQIDApDYWxpZm9ybmlhMRUwEwYDVQQKDAxPbmVsb2dpbiBJbmMxFzAVBgNVBAMMDnNwLmV4YW1wbGUuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDZx+ON4IUoIWxgukTb1tOiX3bMYzYQiwWPUNMp+Fq82xoNogso2bykZG0yiJm5o8zv/sd6pGouayMgkx/2FSOdc36T0jGbCHuRSbtia0PEzNIRtmViMrt3AeoWBidRXmZsxCNLwgIV6dn2WpuE5Az0bHgpZnQxTKFek0BMKU/d8wIDAQABo1AwTjAdBgNVHQ4EFgQUGHxYqZYyX7cTxKVODVgZwSTdCnwwHwYDVR0jBBgwFoAUGHxYqZYyX7cTxKVODVgZwSTdCnwwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQ0FAAOBgQByFOl+hMFICbd3DJfnp2Rgd/dqttsZG/tyhILWvErbio/DEe98mXpowhTkC04ENprOyXi7ZbUqiicF89uAGyt1oqgTUCD1VsLahqIcmrzgumNyTwLGWo17WDAa1/usDhetWAMhgzF/Cnf5ek0nK00m0YZGyc4LzgD0CROMASTWNg==
-
-
-
-
- http://idp.example.com/metadata.php
-
- _ce3d2948b4cf20146dee0a0b3dd6f69b6cf86f62d7
-
-
-
-
-
-
- http://sp.example.com/demo1/metadata.php
-
-
-
-
- urn:oasis:names:tc:SAML:2.0:ac:classes:Password
-
-
-
-
- test
-
-
- test@example.com
-
-
- users
- examplerole1
-
-
-
-
-");
-
- samlresp.IsValid().ShouldBeTrue();
- samlresp.GetNameID().ShouldBe("_ce3d2948b4cf20146dee0a0b3dd6f69b6cf86f62d7");
- samlresp.GetEmail().ShouldBe("test@example.com");
- samlresp.GetCustomAttribute("uid").ShouldBe("test");
- }
-
- [TestMethod]
- public void TestSamlSignoutResponseValidator()
- {
- //this test's cert and signature borrowed from https://github.com/boxyhq/jackson/
-
- var cert = @"-----BEGIN CERTIFICATE-----
-MIIDBzCCAe+gAwIBAgIJcp0xLOhRU0fTMA0GCSqGSIb3DQEBCwUAMCExHzAdBgNVBAMTFmRldi10eWo3cXl6ei5hdXRoMC5jb20wHhcNMTkwMzI3MTMyMTQ0WhcNMzIxMjAzMTMyMTQ0WjAhMR8wHQYDVQQDExZkZXYtdHlqN3F5enouYXV0aDAuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyr2LHhkTEf5xO+mGjZascQ9bfzcSDmjyJ6RxfD9rAJorqVDIcq+dEtxDvo0HWt/bccX+9AZmMiqCclLRyv7Sley7BkxYra5ym8mTwmaZqUZbWyCQ15Hpq6G27yrWk8V6WKvMhJoxDqlgFh08QDOxBy5jCzwxVyFKDchJiy1TflLC8dFJLcmszQsrvl3enbQyYy9XejgniugJKElZMZknFF9LmcQWeCmwDG+2w6HcMZIXPny9Cl5GZra7wt/EWg3iwNw5ZqP41Hulf9fhilJs3bVehnDgftQTKyTUBEfCDxzaIsEmpPWAqTg5IIEKkHX4/1Rm+7ltxg+n0pIXxUrtCQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRcb2UMMqwD9zCk3DOWnx/XwfKd5DAOBgNVHQ8BAf8EBAMCAoQwDQYJKoZIhvcNAQELBQADggEBAFE1FG/u0eYHk/R5a8gGiPgazEjmQUSMlBxjhhTU8bc0X/oLyCfJGdoXQKJVtHgKAIcvCtrHBjKDy8CwSn+J1jTMZklnpkhvXUHiEj1ViplupwuXblvhEXR2+Bkly57Uy1qoFvKHCejayRWsDaG062kEQkt5k1FtVatUGS6labThHjr8K2RyqTAYpXWqthR+wKTFLni9V2pjuoUOABBYeGTalnIOGvr/i5I+IjJDHND0x7wrveekFDI5yX9V8ZdMGiN2SkoXBMa5+o1aD3gtbi8c2HcOgjMsIzHGAj4dz/0syWfpkEkrbs7FURSvtuRLaNrH/2/rto0KgiWWuPKvm1w=
------END CERTIFICATE-----";
-
- var samlresp = new Saml.SignoutResponse(cert);
- samlresp.LoadXml(@"urn:dev-tyj7qyzz.auth0.comLk9TO/DGFFLLb+29H32O/scFccU=altTmKkKqudi+jYBZd6bETdYRbTKerUiNxFugcoD7ZmdZsRlrcNir0ZLRq+NB6nTh4zeKwGiGs03FyAW0Wdr8vgl0GQ/KOGuUrpoFNI8EID1HYrghHZMR43CgauIHGg0dw8uSjQYUcU1ICVYG2trgXC9TR81g+3XVBPBnoJWS2yV8hPc6QdFAUdb/0qUn/GPdpSPOlb6/MMUQB+K+es6HzjQfU2PEV3aNarHrKHSyFRdBHFMgtt7rUE3eAev+3/Uwq6RPBFk9huUJ6F0MRDoVjpWNzD2jByTtRv7OYInDsEJKCwJ+6pOKGVK6GDXuXnuI8s6BNEalpNJkWR8BxFVbw==MIIDBzCCAe+gAwIBAgIJcp0xLOhRU0fTMA0GCSqGSIb3DQEBCwUAMCExHzAdBgNVBAMTFmRldi10eWo3cXl6ei5hdXRoMC5jb20wHhcNMTkwMzI3MTMyMTQ0WhcNMzIxMjAzMTMyMTQ0WjAhMR8wHQYDVQQDExZkZXYtdHlqN3F5enouYXV0aDAuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyr2LHhkTEf5xO+mGjZascQ9bfzcSDmjyJ6RxfD9rAJorqVDIcq+dEtxDvo0HWt/bccX+9AZmMiqCclLRyv7Sley7BkxYra5ym8mTwmaZqUZbWyCQ15Hpq6G27yrWk8V6WKvMhJoxDqlgFh08QDOxBy5jCzwxVyFKDchJiy1TflLC8dFJLcmszQsrvl3enbQyYy9XejgniugJKElZMZknFF9LmcQWeCmwDG+2w6HcMZIXPny9Cl5GZra7wt/EWg3iwNw5ZqP41Hulf9fhilJs3bVehnDgftQTKyTUBEfCDxzaIsEmpPWAqTg5IIEKkHX4/1Rm+7ltxg+n0pIXxUrtCQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRcb2UMMqwD9zCk3DOWnx/XwfKd5DAOBgNVHQ8BAf8EBAMCAoQwDQYJKoZIhvcNAQELBQADggEBAFE1FG/u0eYHk/R5a8gGiPgazEjmQUSMlBxjhhTU8bc0X/oLyCfJGdoXQKJVtHgKAIcvCtrHBjKDy8CwSn+J1jTMZklnpkhvXUHiEj1ViplupwuXblvhEXR2+Bkly57Uy1qoFvKHCejayRWsDaG062kEQkt5k1FtVatUGS6labThHjr8K2RyqTAYpXWqthR+wKTFLni9V2pjuoUOABBYeGTalnIOGvr/i5I+IjJDHND0x7wrveekFDI5yX9V8ZdMGiN2SkoXBMa5+o1aD3gtbi8c2HcOgjMsIzHGAj4dz/0syWfpkEkrbs7FURSvtuRLaNrH/2/rto0KgiWWuPKvm1w=");
-
-
- samlresp.IsValid().ShouldBeTrue();
- samlresp.GetLogoutStatus().ShouldBe("Success");
- }
-
- [TestMethod]
- public void TestSamlResponseValidatorAdvanced()
- {
- var cert = @"-----BEGIN CERTIFICATE-----
-MIIClTCCAX0CBgGICgolYzANBgkqhkiG9w0BAQsFADAOMQwwCgYDVQQDDANQT0MwHhcNMjMwNTExMDg1ODM3WhcNMzMwNTExMDkwMDE3WjAOMQwwCgYDVQQDDANQT0MwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCdKUug5y3ifMXH2kPGPib3APKzA1n9GEsAV304irs9oKK91iCpmQL0SfmMRtyWILPUTSSfKb+Ius2U9AgcjIs517DsbZYTZAglpuZ1DUZTN4IM2PRBrt2bpKv8vQTplesKw6QnWFGrjlOPtw1UmsTnciqiy71GHssSNlLvMObpyW02tt0mGbWQRvCeIwt+aXTB2xrK7buBNJ8yUwdJ0VOpfsUR0yLmV2N/oN0F+f1I/kxn/COEgFZiqJWWEyRCMCXafetU+dq8YMtcO149CKxK66WgTyanAjBf2jv7v5Gk3/0vrLFEIPtHBonDFFQeGw/sTV6bJG+tIS1CX5R/guZRAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAEdXFmQ0BNE4IrE+aEueIl/eyyb90jdU1gmtlrqIvR+RsuQlJzasjE5qW1vcZTdV+omQpeePnIY94KwkhbWwaMsshq7Zi7bbyNWmhc0Mo3o6ONbr3Q6fvfNBePbObGfVCFRT3mgwiqrR59Wmku4PopRS/DXYvbQoim5rxiClAHyN0PkcX6u5J7mmzV1RiZ5OE4fJkIHXXmvUc6NeeFOx8EUnEDrVbfyBn9AK0IZAoj7/jKAJPv5DsBZH3iuFwjSOCAIkpr3W0YcITBeRAvdAri9eFpJ3GO1ZKjynpQaUNWeB3JBjJeNBfQszzmEHlv3Lrayiv2+/uTjFZ2DT7jfxaMw=
------END CERTIFICATE-----";
-
- var samlresp = new Saml.Response(cert);
- samlresp.LoadXml(@"http://keycloak:1080/realms/POCUrJzr9Ja0f4Ks+K6TPEfQ53bw1veGXHtMZpLmRrr/ww=EAM65nY/e0YkK/H0nw+hdt6PhUIEs5jtftvP/NuHCSFjsVNj8L4jIT7Gvso8r9gSnwz0FJetVK16LjHdN+0f8Od2BDk9njD7KBQx9v9ich12zl1Ny+T6dLtc4XypkvoPwscna7KIQOEn8xeKBq4IbC+gPYfJEQ3GjnQ5JuXhJW5GValLELKWbH21oECRL6VAs7BAohQy2/BbTTGM1tbeuqWIZrqdP/KKOpiHxVIPwzwC8EuQmrhYiaJ9tOzNtBJGD5IW7L6Z6GIhVX2yQPuEW/gfb/bYCi6+0KD664YBICfyJLSarbcK6qgafP9YUdJ48qopiHXbuZ1m8ceCfC0Kow==MIIClTCCAX0CBgGICgolYzANBgkqhkiG9w0BAQsFADAOMQwwCgYDVQQDDANQT0MwHhcNMjMwNTExMDg1ODM3WhcNMzMwNTExMDkwMDE3WjAOMQwwCgYDVQQDDANQT0MwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCdKUug5y3ifMXH2kPGPib3APKzA1n9GEsAV304irs9oKK91iCpmQL0SfmMRtyWILPUTSSfKb+Ius2U9AgcjIs517DsbZYTZAglpuZ1DUZTN4IM2PRBrt2bpKv8vQTplesKw6QnWFGrjlOPtw1UmsTnciqiy71GHssSNlLvMObpyW02tt0mGbWQRvCeIwt+aXTB2xrK7buBNJ8yUwdJ0VOpfsUR0yLmV2N/oN0F+f1I/kxn/COEgFZiqJWWEyRCMCXafetU+dq8YMtcO149CKxK66WgTyanAjBf2jv7v5Gk3/0vrLFEIPtHBonDFFQeGw/sTV6bJG+tIS1CX5R/guZRAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAEdXFmQ0BNE4IrE+aEueIl/eyyb90jdU1gmtlrqIvR+RsuQlJzasjE5qW1vcZTdV+omQpeePnIY94KwkhbWwaMsshq7Zi7bbyNWmhc0Mo3o6ONbr3Q6fvfNBePbObGfVCFRT3mgwiqrR59Wmku4PopRS/DXYvbQoim5rxiClAHyN0PkcX6u5J7mmzV1RiZ5OE4fJkIHXXmvUc6NeeFOx8EUnEDrVbfyBn9AK0IZAoj7/jKAJPv5DsBZH3iuFwjSOCAIkpr3W0YcITBeRAvdAri9eFpJ3GO1ZKjynpQaUNWeB3JBjJeNBfQszzmEHlv3Lrayiv2+/uTjFZ2DT7jfxaMw=http://keycloak:1080/realms/POCguestWebApp3urn:oasis:names:tc:SAML:2.0:ac:classes:unspecifiedguest@guest.comGuestGuestuma_authorizationoffline_accessdefault-roles-pocview-profilemanage-accountmanage-account-linksSimpleUser");
-
- samlresp.IsValid().ShouldBeTrue();
- samlresp.GetCustomAttributeViaFriendlyName("givenName").ShouldBe("Guest");
- samlresp.GetCustomAttributeAsList("Role").ShouldBe(new List { "uma_authorization", "offline_access", "default-roles-poc", "view-profile", "manage-account", "manage-account-links", "SimpleUser" }, ignoreOrder: true);
- }
-
- [TestMethod]
- public void TestSamlRequest()
- {
- var request = new AuthRequest(
- "http://www.myapp.com",
- "http://www.myapp.com/SamlConsume"
- );
-
- var r = request.GetRequest();
-
- //decode the compressed base64
- var ms = new MemoryStream(Convert.FromBase64String(r));
- var ds = new DeflateStream(ms, CompressionMode.Decompress, true);
- var output = new MemoryStream();
- ds.CopyTo(output);
-
- //get xml
- var str = Encoding.UTF8.GetString(output.ToArray());
-
- str.ShouldEndWith(@"ProtocolBinding=""urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"" AssertionConsumerServiceURL=""http://www.myapp.com/SamlConsume"" xmlns:samlp=""urn:oasis:names:tc:SAML:2.0:protocol"">http://www.myapp.com");
- }
-
- [TestMethod]
- public void TestStringToByteArray()
- {
- //test that the old StringToByteArray was generating same result as the new Encoding.ASCII.GetBytes
-
- var cert = @"-----BEGIN CERTIFICATE-----
-MIICajCCAdOgAwIBAgIBADANBgkqhkiG9w0BAQ0FADBSMQswCQYDVQQGEwJ1czETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMT25lbG9naW4gSW5jMRcwFQYDVQQDDA5zcC5leGFtcGxlLmNvbTAeFw0xNDA3MTcxNDEyNTZaFw0xNTA3MTcxNDEyNTZaMFIxCzAJBgNVBAYTAnVzMRMwEQYDVQQIDApDYWxpZm9ybmlhMRUwEwYDVQQKDAxPbmVsb2dpbiBJbmMxFzAVBgNVBAMMDnNwLmV4YW1wbGUuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDZx+ON4IUoIWxgukTb1tOiX3bMYzYQiwWPUNMp+Fq82xoNogso2bykZG0yiJm5o8zv/sd6pGouayMgkx/2FSOdc36T0jGbCHuRSbtia0PEzNIRtmViMrt3AeoWBidRXmZsxCNLwgIV6dn2WpuE5Az0bHgpZnQxTKFek0BMKU/d8wIDAQABo1AwTjAdBgNVHQ4EFgQUGHxYqZYyX7cTxKVODVgZwSTdCnwwHwYDVR0jBBgwFoAUGHxYqZYyX7cTxKVODVgZwSTdCnwwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQ0FAAOBgQByFOl+hMFICbd3DJfnp2Rgd/dqttsZG/tyhILWvErbio/DEe98mXpowhTkC04ENprOyXi7ZbUqiicF89uAGyt1oqgTUCD1VsLahqIcmrzgumNyTwLGWo17WDAa1/usDhetWAMhgzF/Cnf5ek0nK00m0YZGyc4LzgD0CROMASTWNg==
------END CERTIFICATE-----";
-
-
- var x = StringToByteArray(cert);
- var y = Encoding.ASCII.GetBytes(cert);
-
- x.SequenceEqual(y).ShouldBeTrue();
- }
-
- [TestMethod]
- public void TestEncryptedAssertions()
- {
- // SAML values from https://www.samltool.com/generic_sso_res.php.
-
- var cert = Constants.Certificates.Certificate;
-
- var samlresp = new Saml.Response(cert);
-
- var xml = @$"
-
- http://idp.example.com/metadata.php
-
-
-
-
-
-
-
-
-
-
- Pn5IVvMXk8cdvEJHQ0VGq9WMOaV2dg4QbuCdEt8Pc1yWZLUMlOghPK0pMevLsuKyBcUz/cIoQihsroBrQONrtLzhdqndGCtaZYoOdO2Lz0T5Huesqd6iEKihrtsLf4RGj2VX3XbtdQV5R/3IdnjGCgj4zClxtJb4P7gCApeQ/uIpjIuo/f1rwn9F0A+gbL5HOSicOrLMjTJVBwPR2EtwY1g7fomkKQtJpWiq2+LsXLoSwWIYM4wHyem6U+zX9qTr2yRefiNuyz1Ye0QCN1LXQCIYFrS0Mhao4MqXNXzkktmI1/FcAbGAwReUkAGY2UuS6+9MtPDuRFOk+8h+ldrxJBU=
-
-
-
-
- WDObtBFd84WFugFF97T0SM3jd0QE6UPhVaiaLJsWRE9/rWN2oF7d0TfiYN9RmbcWYVMVdxl26o2QMX7nKv+ufesu+GSEMApKOKKjYqGYIWvSsnoeqZGoXftjl7+axLAt7XAqT4edh4IhaxM4k3aPdEFfc+fZVNzr9djUcOF7l7tFT29M0zeO/K/y6m9lvaWiRvdLf1K1Wqw8eramYvE7FhomwbIeWJguHznKrAfxhqw6HifIot/ox1pKpmyP49HLvq5tWQexTS+iNyktXzv0wZDOKjtfOy5xd5L8iXVBhY29a0tiFcnVrEWKZ7Z/kTKrl6uuxtiD6qOmlLQpcoSc1DeXnooBJn/PhIbsQZo6uKTtzMmRc62R3d32JZRUrg/Bpjtcb6nB4Iz4SSw4gSm4w7aNGKX3DqYpTAseEg082wtY4ZX8wTcb0pRV5Gc/h7vRNGtqD1q8/gmhQdpRZ468lg==
-
-
-
-
-
-
-
-
-
-
- Pn5IVvMXk8cdvEJHQ0VGq9WMOaV2dg4QbuCdEt8Pc1yWZLUMlOghPK0pMevLsuKyBcUz/cIoQihsroBrQONrtLzhdqndGCtaZYoOdO2Lz0T5Huesqd6iEKihrtsLf4RGj2VX3XbtdQV5R/3IdnjGCgj4zClxtJb4P7gCApeQ/uIpjIuo/f1rwn9F0A+gbL5HOSicOrLMjTJVBwPR2EtwY1g7fomkKQtJpWiq2+LsXLoSwWIYM4wHyem6U+zX9qTr2yRefiNuyz1Ye0QCN1LXQCIYFrS0Mhao4MqXNXzkktmI1/FcAbGAwReUkAGY2UuS6+9MtPDuRFOk+8h+ldrxJBU=
-
-
-
-
- WDObtBFd84WFugFF97T0SM3jd0QE6UPhVaiaLJsWRE9/rWN2oF7d0TfiYN9RmbcWYVMVdxl26o2QMX7nKv+ufesu+GSEMApKOKKjYqGYIWvSsnoeqZGoXftjl7+axLAt7XAqT4edh4IhaxM4k3aPdEFfc+fZVNzr9djUcOF7l7tFT29M0zeO/K/y6m9lvaWiRvdLf1K1Wqw8eramYvE7FhomwbIeWJguHznKrAfxhqw6HifIot/ox1pKpmyP49HLvq5tWQexTS+iNyktXzv0wZDOKjtfOy5xd5L8iXVBhY29a0tiFcnVrEWKZ7Z/kTKrl6uuxtiD6qOmlLQpcoSc1DeXnooBJn/PhIbsQZo6uKTtzMmRc62R3d32JZRUrg/Bpjtcb6nB4Iz4SSw4gSm4w7aNGKX3DqYpTAseEg082wtY4ZX8wTcb0pRV5Gc/h7vRNGtqD1q8/gmhQdpRZ468lg==
-
-
-
- ";
-
- samlresp.LoadXml(xml);
-
- var attributes = samlresp.GetEncryptedAttributes();
-
- attributes.ShouldNotBeEmpty();
-
- var expectedValues = new[] {
- (ClaimTypes.MobilePhone, "555-555-1234"),
- (ClaimTypes.MobilePhone, "555-555-4321"),
- (ClaimTypes.MobilePhone, "555-555-1234"),
- (ClaimTypes.MobilePhone, "555-555-4321")
- };
-
- attributes.ShouldBe(expectedValues);
-
- // The results can be filtered by claim type.
- attributes.Where(x => x.Name == ClaimTypes.MobilePhone).ShouldBe(expectedValues);
- attributes.Where(x => x.Name == ClaimTypes.Email).ShouldBeEmpty();
- }
-
- private static byte[] StringToByteArray(string st)
- {
- byte[] bytes = new byte[st.Length];
- for (int i = 0; i < st.Length; i++)
- {
- bytes[i] = (byte)st[i];
- }
- return bytes;
- }
- }
-}
+using Saml;
+using System.IO.Compression;
+using System.Text;
+using Shouldly;
+using System.Security.Claims;
+
+namespace AspNetSaml.Tests
+{
+ [TestClass]
+ public class UnitTests
+ {
+ //cert and signature taken form here: www.samltool.com/generic_sso_res.php
+
+ [TestMethod]
+ public void TestSamlResponseValidator()
+ {
+ var cert = @"-----BEGIN CERTIFICATE-----
+MIICajCCAdOgAwIBAgIBADANBgkqhkiG9w0BAQ0FADBSMQswCQYDVQQGEwJ1czETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMT25lbG9naW4gSW5jMRcwFQYDVQQDDA5zcC5leGFtcGxlLmNvbTAeFw0xNDA3MTcxNDEyNTZaFw0xNTA3MTcxNDEyNTZaMFIxCzAJBgNVBAYTAnVzMRMwEQYDVQQIDApDYWxpZm9ybmlhMRUwEwYDVQQKDAxPbmVsb2dpbiBJbmMxFzAVBgNVBAMMDnNwLmV4YW1wbGUuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDZx+ON4IUoIWxgukTb1tOiX3bMYzYQiwWPUNMp+Fq82xoNogso2bykZG0yiJm5o8zv/sd6pGouayMgkx/2FSOdc36T0jGbCHuRSbtia0PEzNIRtmViMrt3AeoWBidRXmZsxCNLwgIV6dn2WpuE5Az0bHgpZnQxTKFek0BMKU/d8wIDAQABo1AwTjAdBgNVHQ4EFgQUGHxYqZYyX7cTxKVODVgZwSTdCnwwHwYDVR0jBBgwFoAUGHxYqZYyX7cTxKVODVgZwSTdCnwwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQ0FAAOBgQByFOl+hMFICbd3DJfnp2Rgd/dqttsZG/tyhILWvErbio/DEe98mXpowhTkC04ENprOyXi7ZbUqiicF89uAGyt1oqgTUCD1VsLahqIcmrzgumNyTwLGWo17WDAa1/usDhetWAMhgzF/Cnf5ek0nK00m0YZGyc4LzgD0CROMASTWNg==
+-----END CERTIFICATE-----";
+
+ var samlresp = new Saml.Response(cert);
+ samlresp.LoadXml(@"
+
+ http://idp.example.com/metadata.php
+
+
+ 99Bke1BpL1yOfGd5ADkGSle2sZg=OOyb3YtYQm3DC7gj6lQPM20r76HH4KvAE93f5xrIuIHGk8ZJlse4m8t4msLkhwUEAGwWOOVyHs8gChtN1m/P4pKCXyttO9Hev14Wz8E1R444kg5Yak+02FZ+Fn3VbbPq+kY4eYRkczNMphivWkdwc/QjDguNzGoKCEEtbBKDMGg=
+MIICajCCAdOgAwIBAgIBADANBgkqhkiG9w0BAQ0FADBSMQswCQYDVQQGEwJ1czETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMT25lbG9naW4gSW5jMRcwFQYDVQQDDA5zcC5leGFtcGxlLmNvbTAeFw0xNDA3MTcxNDEyNTZaFw0xNTA3MTcxNDEyNTZaMFIxCzAJBgNVBAYTAnVzMRMwEQYDVQQIDApDYWxpZm9ybmlhMRUwEwYDVQQKDAxPbmVsb2dpbiBJbmMxFzAVBgNVBAMMDnNwLmV4YW1wbGUuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDZx+ON4IUoIWxgukTb1tOiX3bMYzYQiwWPUNMp+Fq82xoNogso2bykZG0yiJm5o8zv/sd6pGouayMgkx/2FSOdc36T0jGbCHuRSbtia0PEzNIRtmViMrt3AeoWBidRXmZsxCNLwgIV6dn2WpuE5Az0bHgpZnQxTKFek0BMKU/d8wIDAQABo1AwTjAdBgNVHQ4EFgQUGHxYqZYyX7cTxKVODVgZwSTdCnwwHwYDVR0jBBgwFoAUGHxYqZYyX7cTxKVODVgZwSTdCnwwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQ0FAAOBgQByFOl+hMFICbd3DJfnp2Rgd/dqttsZG/tyhILWvErbio/DEe98mXpowhTkC04ENprOyXi7ZbUqiicF89uAGyt1oqgTUCD1VsLahqIcmrzgumNyTwLGWo17WDAa1/usDhetWAMhgzF/Cnf5ek0nK00m0YZGyc4LzgD0CROMASTWNg==
+
+
+
+
+ http://idp.example.com/metadata.php
+
+ _ce3d2948b4cf20146dee0a0b3dd6f69b6cf86f62d7
+
+
+
+
+
+
+ http://sp.example.com/demo1/metadata.php
+
+
+
+
+ urn:oasis:names:tc:SAML:2.0:ac:classes:Password
+
+
+
+
+ test
+
+
+ test@example.com
+
+
+ users
+ examplerole1
+
+
+
+
+");
+
+ samlresp.IsValid().ShouldBeTrue();
+ samlresp.GetNameID().ShouldBe("_ce3d2948b4cf20146dee0a0b3dd6f69b6cf86f62d7");
+ samlresp.GetEmail().ShouldBe("test@example.com");
+ samlresp.GetCustomAttribute("uid").ShouldBe("test");
+ }
+
+ [TestMethod]
+ public void TestSamlSignoutResponseValidator()
+ {
+ //this test's cert and signature borrowed from https://github.com/boxyhq/jackson/
+
+ var cert = @"-----BEGIN CERTIFICATE-----
+MIIDBzCCAe+gAwIBAgIJcp0xLOhRU0fTMA0GCSqGSIb3DQEBCwUAMCExHzAdBgNVBAMTFmRldi10eWo3cXl6ei5hdXRoMC5jb20wHhcNMTkwMzI3MTMyMTQ0WhcNMzIxMjAzMTMyMTQ0WjAhMR8wHQYDVQQDExZkZXYtdHlqN3F5enouYXV0aDAuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyr2LHhkTEf5xO+mGjZascQ9bfzcSDmjyJ6RxfD9rAJorqVDIcq+dEtxDvo0HWt/bccX+9AZmMiqCclLRyv7Sley7BkxYra5ym8mTwmaZqUZbWyCQ15Hpq6G27yrWk8V6WKvMhJoxDqlgFh08QDOxBy5jCzwxVyFKDchJiy1TflLC8dFJLcmszQsrvl3enbQyYy9XejgniugJKElZMZknFF9LmcQWeCmwDG+2w6HcMZIXPny9Cl5GZra7wt/EWg3iwNw5ZqP41Hulf9fhilJs3bVehnDgftQTKyTUBEfCDxzaIsEmpPWAqTg5IIEKkHX4/1Rm+7ltxg+n0pIXxUrtCQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRcb2UMMqwD9zCk3DOWnx/XwfKd5DAOBgNVHQ8BAf8EBAMCAoQwDQYJKoZIhvcNAQELBQADggEBAFE1FG/u0eYHk/R5a8gGiPgazEjmQUSMlBxjhhTU8bc0X/oLyCfJGdoXQKJVtHgKAIcvCtrHBjKDy8CwSn+J1jTMZklnpkhvXUHiEj1ViplupwuXblvhEXR2+Bkly57Uy1qoFvKHCejayRWsDaG062kEQkt5k1FtVatUGS6labThHjr8K2RyqTAYpXWqthR+wKTFLni9V2pjuoUOABBYeGTalnIOGvr/i5I+IjJDHND0x7wrveekFDI5yX9V8ZdMGiN2SkoXBMa5+o1aD3gtbi8c2HcOgjMsIzHGAj4dz/0syWfpkEkrbs7FURSvtuRLaNrH/2/rto0KgiWWuPKvm1w=
+-----END CERTIFICATE-----";
+
+ var samlresp = new Saml.SignoutResponse(cert);
+ samlresp.LoadXml(@"urn:dev-tyj7qyzz.auth0.comLk9TO/DGFFLLb+29H32O/scFccU=altTmKkKqudi+jYBZd6bETdYRbTKerUiNxFugcoD7ZmdZsRlrcNir0ZLRq+NB6nTh4zeKwGiGs03FyAW0Wdr8vgl0GQ/KOGuUrpoFNI8EID1HYrghHZMR43CgauIHGg0dw8uSjQYUcU1ICVYG2trgXC9TR81g+3XVBPBnoJWS2yV8hPc6QdFAUdb/0qUn/GPdpSPOlb6/MMUQB+K+es6HzjQfU2PEV3aNarHrKHSyFRdBHFMgtt7rUE3eAev+3/Uwq6RPBFk9huUJ6F0MRDoVjpWNzD2jByTtRv7OYInDsEJKCwJ+6pOKGVK6GDXuXnuI8s6BNEalpNJkWR8BxFVbw==MIIDBzCCAe+gAwIBAgIJcp0xLOhRU0fTMA0GCSqGSIb3DQEBCwUAMCExHzAdBgNVBAMTFmRldi10eWo3cXl6ei5hdXRoMC5jb20wHhcNMTkwMzI3MTMyMTQ0WhcNMzIxMjAzMTMyMTQ0WjAhMR8wHQYDVQQDExZkZXYtdHlqN3F5enouYXV0aDAuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyr2LHhkTEf5xO+mGjZascQ9bfzcSDmjyJ6RxfD9rAJorqVDIcq+dEtxDvo0HWt/bccX+9AZmMiqCclLRyv7Sley7BkxYra5ym8mTwmaZqUZbWyCQ15Hpq6G27yrWk8V6WKvMhJoxDqlgFh08QDOxBy5jCzwxVyFKDchJiy1TflLC8dFJLcmszQsrvl3enbQyYy9XejgniugJKElZMZknFF9LmcQWeCmwDG+2w6HcMZIXPny9Cl5GZra7wt/EWg3iwNw5ZqP41Hulf9fhilJs3bVehnDgftQTKyTUBEfCDxzaIsEmpPWAqTg5IIEKkHX4/1Rm+7ltxg+n0pIXxUrtCQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRcb2UMMqwD9zCk3DOWnx/XwfKd5DAOBgNVHQ8BAf8EBAMCAoQwDQYJKoZIhvcNAQELBQADggEBAFE1FG/u0eYHk/R5a8gGiPgazEjmQUSMlBxjhhTU8bc0X/oLyCfJGdoXQKJVtHgKAIcvCtrHBjKDy8CwSn+J1jTMZklnpkhvXUHiEj1ViplupwuXblvhEXR2+Bkly57Uy1qoFvKHCejayRWsDaG062kEQkt5k1FtVatUGS6labThHjr8K2RyqTAYpXWqthR+wKTFLni9V2pjuoUOABBYeGTalnIOGvr/i5I+IjJDHND0x7wrveekFDI5yX9V8ZdMGiN2SkoXBMa5+o1aD3gtbi8c2HcOgjMsIzHGAj4dz/0syWfpkEkrbs7FURSvtuRLaNrH/2/rto0KgiWWuPKvm1w=");
+
+
+ samlresp.IsValid().ShouldBeTrue();
+ samlresp.GetLogoutStatus().ShouldBe("Success");
+ }
+
+ [TestMethod]
+ public void TestSamlResponseValidatorAdvanced()
+ {
+ var cert = @"-----BEGIN CERTIFICATE-----
+MIIClTCCAX0CBgGICgolYzANBgkqhkiG9w0BAQsFADAOMQwwCgYDVQQDDANQT0MwHhcNMjMwNTExMDg1ODM3WhcNMzMwNTExMDkwMDE3WjAOMQwwCgYDVQQDDANQT0MwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCdKUug5y3ifMXH2kPGPib3APKzA1n9GEsAV304irs9oKK91iCpmQL0SfmMRtyWILPUTSSfKb+Ius2U9AgcjIs517DsbZYTZAglpuZ1DUZTN4IM2PRBrt2bpKv8vQTplesKw6QnWFGrjlOPtw1UmsTnciqiy71GHssSNlLvMObpyW02tt0mGbWQRvCeIwt+aXTB2xrK7buBNJ8yUwdJ0VOpfsUR0yLmV2N/oN0F+f1I/kxn/COEgFZiqJWWEyRCMCXafetU+dq8YMtcO149CKxK66WgTyanAjBf2jv7v5Gk3/0vrLFEIPtHBonDFFQeGw/sTV6bJG+tIS1CX5R/guZRAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAEdXFmQ0BNE4IrE+aEueIl/eyyb90jdU1gmtlrqIvR+RsuQlJzasjE5qW1vcZTdV+omQpeePnIY94KwkhbWwaMsshq7Zi7bbyNWmhc0Mo3o6ONbr3Q6fvfNBePbObGfVCFRT3mgwiqrR59Wmku4PopRS/DXYvbQoim5rxiClAHyN0PkcX6u5J7mmzV1RiZ5OE4fJkIHXXmvUc6NeeFOx8EUnEDrVbfyBn9AK0IZAoj7/jKAJPv5DsBZH3iuFwjSOCAIkpr3W0YcITBeRAvdAri9eFpJ3GO1ZKjynpQaUNWeB3JBjJeNBfQszzmEHlv3Lrayiv2+/uTjFZ2DT7jfxaMw=
+-----END CERTIFICATE-----";
+
+ var samlresp = new Saml.Response(cert);
+ samlresp.LoadXml(@"http://keycloak:1080/realms/POCUrJzr9Ja0f4Ks+K6TPEfQ53bw1veGXHtMZpLmRrr/ww=EAM65nY/e0YkK/H0nw+hdt6PhUIEs5jtftvP/NuHCSFjsVNj8L4jIT7Gvso8r9gSnwz0FJetVK16LjHdN+0f8Od2BDk9njD7KBQx9v9ich12zl1Ny+T6dLtc4XypkvoPwscna7KIQOEn8xeKBq4IbC+gPYfJEQ3GjnQ5JuXhJW5GValLELKWbH21oECRL6VAs7BAohQy2/BbTTGM1tbeuqWIZrqdP/KKOpiHxVIPwzwC8EuQmrhYiaJ9tOzNtBJGD5IW7L6Z6GIhVX2yQPuEW/gfb/bYCi6+0KD664YBICfyJLSarbcK6qgafP9YUdJ48qopiHXbuZ1m8ceCfC0Kow==MIIClTCCAX0CBgGICgolYzANBgkqhkiG9w0BAQsFADAOMQwwCgYDVQQDDANQT0MwHhcNMjMwNTExMDg1ODM3WhcNMzMwNTExMDkwMDE3WjAOMQwwCgYDVQQDDANQT0MwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCdKUug5y3ifMXH2kPGPib3APKzA1n9GEsAV304irs9oKK91iCpmQL0SfmMRtyWILPUTSSfKb+Ius2U9AgcjIs517DsbZYTZAglpuZ1DUZTN4IM2PRBrt2bpKv8vQTplesKw6QnWFGrjlOPtw1UmsTnciqiy71GHssSNlLvMObpyW02tt0mGbWQRvCeIwt+aXTB2xrK7buBNJ8yUwdJ0VOpfsUR0yLmV2N/oN0F+f1I/kxn/COEgFZiqJWWEyRCMCXafetU+dq8YMtcO149CKxK66WgTyanAjBf2jv7v5Gk3/0vrLFEIPtHBonDFFQeGw/sTV6bJG+tIS1CX5R/guZRAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAEdXFmQ0BNE4IrE+aEueIl/eyyb90jdU1gmtlrqIvR+RsuQlJzasjE5qW1vcZTdV+omQpeePnIY94KwkhbWwaMsshq7Zi7bbyNWmhc0Mo3o6ONbr3Q6fvfNBePbObGfVCFRT3mgwiqrR59Wmku4PopRS/DXYvbQoim5rxiClAHyN0PkcX6u5J7mmzV1RiZ5OE4fJkIHXXmvUc6NeeFOx8EUnEDrVbfyBn9AK0IZAoj7/jKAJPv5DsBZH3iuFwjSOCAIkpr3W0YcITBeRAvdAri9eFpJ3GO1ZKjynpQaUNWeB3JBjJeNBfQszzmEHlv3Lrayiv2+/uTjFZ2DT7jfxaMw=http://keycloak:1080/realms/POCguestWebApp3urn:oasis:names:tc:SAML:2.0:ac:classes:unspecifiedguest@guest.comGuestGuestuma_authorizationoffline_accessdefault-roles-pocview-profilemanage-accountmanage-account-linksSimpleUser");
+
+ samlresp.IsValid().ShouldBeTrue();
+ samlresp.GetCustomAttributeViaFriendlyName("givenName").ShouldBe("Guest");
+ samlresp.GetCustomAttributeAsList("Role").ShouldBe(new List { "uma_authorization", "offline_access", "default-roles-poc", "view-profile", "manage-account", "manage-account-links", "SimpleUser" }, ignoreOrder: true);
+ }
+
+ [TestMethod]
+ public void TestSamlRequest()
+ {
+ var request = new AuthRequest(
+ "http://www.myapp.com",
+ "http://www.myapp.com/SamlConsume"
+ );
+
+ var r = request.GetRequest();
+
+ //decode the compressed base64
+ var ms = new MemoryStream(Convert.FromBase64String(r));
+ var ds = new DeflateStream(ms, CompressionMode.Decompress, true);
+ var output = new MemoryStream();
+ ds.CopyTo(output);
+
+ //get xml
+ var str = Encoding.UTF8.GetString(output.ToArray());
+
+ str.ShouldEndWith(@"ProtocolBinding=""urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"" AssertionConsumerServiceURL=""http://www.myapp.com/SamlConsume"" xmlns:samlp=""urn:oasis:names:tc:SAML:2.0:protocol"">http://www.myapp.com");
+ }
+
+ [TestMethod]
+ public void TestStringToByteArray()
+ {
+ //test that the old StringToByteArray was generating same result as the new Encoding.ASCII.GetBytes
+
+ var cert = @"-----BEGIN CERTIFICATE-----
+MIICajCCAdOgAwIBAgIBADANBgkqhkiG9w0BAQ0FADBSMQswCQYDVQQGEwJ1czETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMT25lbG9naW4gSW5jMRcwFQYDVQQDDA5zcC5leGFtcGxlLmNvbTAeFw0xNDA3MTcxNDEyNTZaFw0xNTA3MTcxNDEyNTZaMFIxCzAJBgNVBAYTAnVzMRMwEQYDVQQIDApDYWxpZm9ybmlhMRUwEwYDVQQKDAxPbmVsb2dpbiBJbmMxFzAVBgNVBAMMDnNwLmV4YW1wbGUuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDZx+ON4IUoIWxgukTb1tOiX3bMYzYQiwWPUNMp+Fq82xoNogso2bykZG0yiJm5o8zv/sd6pGouayMgkx/2FSOdc36T0jGbCHuRSbtia0PEzNIRtmViMrt3AeoWBidRXmZsxCNLwgIV6dn2WpuE5Az0bHgpZnQxTKFek0BMKU/d8wIDAQABo1AwTjAdBgNVHQ4EFgQUGHxYqZYyX7cTxKVODVgZwSTdCnwwHwYDVR0jBBgwFoAUGHxYqZYyX7cTxKVODVgZwSTdCnwwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQ0FAAOBgQByFOl+hMFICbd3DJfnp2Rgd/dqttsZG/tyhILWvErbio/DEe98mXpowhTkC04ENprOyXi7ZbUqiicF89uAGyt1oqgTUCD1VsLahqIcmrzgumNyTwLGWo17WDAa1/usDhetWAMhgzF/Cnf5ek0nK00m0YZGyc4LzgD0CROMASTWNg==
+-----END CERTIFICATE-----";
+
+
+ var x = StringToByteArray(cert);
+ var y = Encoding.ASCII.GetBytes(cert);
+
+ x.SequenceEqual(y).ShouldBeTrue();
+ }
+
+ [TestMethod]
+ public void TestEncryptedAssertions()
+ {
+ // SAML values from https://www.samltool.com/generic_sso_res.php.
+
+ var cert = Constants.Certificates.Certificate;
+
+ var samlresp = new Saml.Response(cert);
+
+ var xml = @$"
+
+ http://idp.example.com/metadata.php
+
+
+
+
+
+
+
+
+
+
+ Pn5IVvMXk8cdvEJHQ0VGq9WMOaV2dg4QbuCdEt8Pc1yWZLUMlOghPK0pMevLsuKyBcUz/cIoQihsroBrQONrtLzhdqndGCtaZYoOdO2Lz0T5Huesqd6iEKihrtsLf4RGj2VX3XbtdQV5R/3IdnjGCgj4zClxtJb4P7gCApeQ/uIpjIuo/f1rwn9F0A+gbL5HOSicOrLMjTJVBwPR2EtwY1g7fomkKQtJpWiq2+LsXLoSwWIYM4wHyem6U+zX9qTr2yRefiNuyz1Ye0QCN1LXQCIYFrS0Mhao4MqXNXzkktmI1/FcAbGAwReUkAGY2UuS6+9MtPDuRFOk+8h+ldrxJBU=
+
+
+
+
+ WDObtBFd84WFugFF97T0SM3jd0QE6UPhVaiaLJsWRE9/rWN2oF7d0TfiYN9RmbcWYVMVdxl26o2QMX7nKv+ufesu+GSEMApKOKKjYqGYIWvSsnoeqZGoXftjl7+axLAt7XAqT4edh4IhaxM4k3aPdEFfc+fZVNzr9djUcOF7l7tFT29M0zeO/K/y6m9lvaWiRvdLf1K1Wqw8eramYvE7FhomwbIeWJguHznKrAfxhqw6HifIot/ox1pKpmyP49HLvq5tWQexTS+iNyktXzv0wZDOKjtfOy5xd5L8iXVBhY29a0tiFcnVrEWKZ7Z/kTKrl6uuxtiD6qOmlLQpcoSc1DeXnooBJn/PhIbsQZo6uKTtzMmRc62R3d32JZRUrg/Bpjtcb6nB4Iz4SSw4gSm4w7aNGKX3DqYpTAseEg082wtY4ZX8wTcb0pRV5Gc/h7vRNGtqD1q8/gmhQdpRZ468lg==
+
+
+
+
+
+
+
+
+
+
+ Pn5IVvMXk8cdvEJHQ0VGq9WMOaV2dg4QbuCdEt8Pc1yWZLUMlOghPK0pMevLsuKyBcUz/cIoQihsroBrQONrtLzhdqndGCtaZYoOdO2Lz0T5Huesqd6iEKihrtsLf4RGj2VX3XbtdQV5R/3IdnjGCgj4zClxtJb4P7gCApeQ/uIpjIuo/f1rwn9F0A+gbL5HOSicOrLMjTJVBwPR2EtwY1g7fomkKQtJpWiq2+LsXLoSwWIYM4wHyem6U+zX9qTr2yRefiNuyz1Ye0QCN1LXQCIYFrS0Mhao4MqXNXzkktmI1/FcAbGAwReUkAGY2UuS6+9MtPDuRFOk+8h+ldrxJBU=
+
+
+
+
+ WDObtBFd84WFugFF97T0SM3jd0QE6UPhVaiaLJsWRE9/rWN2oF7d0TfiYN9RmbcWYVMVdxl26o2QMX7nKv+ufesu+GSEMApKOKKjYqGYIWvSsnoeqZGoXftjl7+axLAt7XAqT4edh4IhaxM4k3aPdEFfc+fZVNzr9djUcOF7l7tFT29M0zeO/K/y6m9lvaWiRvdLf1K1Wqw8eramYvE7FhomwbIeWJguHznKrAfxhqw6HifIot/ox1pKpmyP49HLvq5tWQexTS+iNyktXzv0wZDOKjtfOy5xd5L8iXVBhY29a0tiFcnVrEWKZ7Z/kTKrl6uuxtiD6qOmlLQpcoSc1DeXnooBJn/PhIbsQZo6uKTtzMmRc62R3d32JZRUrg/Bpjtcb6nB4Iz4SSw4gSm4w7aNGKX3DqYpTAseEg082wtY4ZX8wTcb0pRV5Gc/h7vRNGtqD1q8/gmhQdpRZ468lg==
+
+
+
+ ";
+
+ samlresp.LoadXml(xml);
+
+ var attributes = samlresp.GetEncryptedAttributes();
+
+ attributes.ShouldNotBeEmpty();
+
+ var expectedValues = new[] {
+ (ClaimTypes.MobilePhone, "555-555-1234"),
+ (ClaimTypes.MobilePhone, "555-555-4321"),
+ (ClaimTypes.MobilePhone, "555-555-1234"),
+ (ClaimTypes.MobilePhone, "555-555-4321")
+ };
+
+ attributes.ShouldBe(expectedValues);
+
+ // The results can be filtered by claim type.
+ attributes.Where(x => x.Name == ClaimTypes.MobilePhone).ShouldBe(expectedValues);
+ attributes.Where(x => x.Name == ClaimTypes.Email).ShouldBeEmpty();
+ }
+
+ private static byte[] StringToByteArray(string st)
+ {
+ byte[] bytes = new byte[st.Length];
+ for (int i = 0; i < st.Length; i++)
+ {
+ bytes[i] = (byte)st[i];
+ }
+ return bytes;
+ }
+ }
+}
diff --git a/AspNetSaml/Saml.cs b/AspNetSaml/Saml.cs
index 2bd4ad5..cc0375a 100644
--- a/AspNetSaml/Saml.cs
+++ b/AspNetSaml/Saml.cs
@@ -1,492 +1,492 @@
-/* Jitbit's simple SAML 2.0 component for ASP.NET
- https://github.com/jitbit/AspNetSaml/
- (c) Jitbit LP, 2016-2023
- Use this freely under the Apache license (see https://choosealicense.com/licenses/apache-2.0/)
-*/
-
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Xml;
-using System.Security.Cryptography.X509Certificates;
-using System.Security.Cryptography.Xml;
-using System.IO.Compression;
-using System.Text;
-using System.Security.Claims;
-using System.Security.Cryptography;
-using System.Xml.Linq;
-
-namespace Saml
-{
- public abstract class BaseResponse
- {
- protected XmlDocument _xmlDoc;
- protected readonly X509Certificate2 _certificate;
- protected XmlNamespaceManager _xmlNameSpaceManager; //we need this one to run our XPath queries on the SAML XML
-
- public string Xml { get { return _xmlDoc.OuterXml; } }
-
- public BaseResponse(string certificateStr, string responseString = null) : this(Encoding.ASCII.GetBytes(certificateStr), responseString) { }
-
- public BaseResponse(byte[] certificateBytes, string responseString = null) : this(new X509Certificate2(certificateBytes), responseString) { }
-
- public BaseResponse(X509Certificate2 certificate, string responseString = null)
- {
- _certificate = certificate;
- if (responseString != null)
- LoadXmlFromBase64(responseString);
- }
-
- ///
- /// Parse SAML response XML (in case was it not passed in constructor)
- ///
- ///
- /// Creates a default namespace manager if one is not provided.
- public void LoadXml(string xml, XmlNamespaceManager namespaceManager = null)
- {
- _xmlDoc = new XmlDocument { PreserveWhitespace = true, XmlResolver = null };
- _xmlDoc.LoadXml(xml);
-
- _xmlNameSpaceManager = namespaceManager ?? GetNamespaceManager(); //lets construct a "manager" for XPath queries
- }
-
- public void LoadXmlFromBase64(string response)
- {
- UTF8Encoding enc = new UTF8Encoding();
- LoadXml(enc.GetString(Convert.FromBase64String(response)));
- }
-
- //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
- protected 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 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;
- }
-
- return true;
- }
-
- //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("xs", "http://www.w3.org/2001/XMLSchema");
- manager.AddNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance");
- manager.AddNamespace("ds", SignedXml.XmlDsigNamespaceUrl);
- manager.AddNamespace("ds", SignedXml.XmlDsigNamespaceUrl);
- manager.AddNamespace("dsig", SignedXml.XmlDsigNamespaceUrl);
- manager.AddNamespace("enc", EncryptedXml.XmlEncNamespaceUrl);
- manager.AddNamespace("xenc", EncryptedXml.XmlEncNamespaceUrl);
- manager.AddNamespace("xmlenc", EncryptedXml.XmlEncNamespaceUrl);
- manager.AddNamespace("saml", "urn:oasis:names:tc:SAML:2.0:assertion");
- manager.AddNamespace("samlp", "urn:oasis:names:tc:SAML:2.0:protocol");
-
- return manager;
- }
-
- ///
- /// Checks the validity of SAML response (validate signature, check expiration date etc)
- ///
- ///
- public bool IsValid()
- {
- XmlNodeList nodeList = _xmlDoc.SelectNodes("//ds:Signature", _xmlNameSpaceManager);
-
- SignedXml signedXml = new SignedXml(_xmlDoc);
-
- if (nodeList.Count == 0) return false;
-
- signedXml.LoadXml((XmlElement)nodeList[0]);
- return ValidateSignatureReference(signedXml) &&
- signedXml.CheckSignature(_certificate, true) &&
- !IsExpired();
- }
-
- 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();
- }
- }
-
- public class Response : BaseResponse
- {
- public Response(string certificateStr, string responseString = null) : base(certificateStr, responseString) { }
-
- public Response(byte[] certificateBytes, string responseString = null) : base(certificateBytes, responseString) { }
-
- public Response(X509Certificate2 certificate, string responseString = null) : base(certificate, responseString) { }
-
- ///
- /// returns the User's login
- ///
- public string GetNameID()
- {
- XmlNode node = _xmlDoc.SelectSingleNode("/samlp:Response/saml:Assertion[1]/saml:Subject/saml:NameID", _xmlNameSpaceManager);
- return node.InnerText;
- }
-
- public virtual string GetUpn()
- {
- return GetCustomAttribute(ClaimTypes.Upn);
- }
-
- public virtual string GetEmail()
- {
- return GetCustomAttribute("User.email")
- ?? GetCustomAttribute(ClaimTypes.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("mail") //some providers put last name into an attribute named "mail"
- ?? GetCustomAttribute("email"); //some providers put last name into an attribute named "email"
- }
-
- public virtual string GetFirstName()
- {
- return GetCustomAttribute("first_name")
- ?? GetCustomAttribute(ClaimTypes.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 GetLastName()
- {
- return GetCustomAttribute("last_name")
- ?? GetCustomAttribute(ClaimTypes.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 GetDepartment()
- {
- return GetCustomAttribute("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/department")
- ?? GetCustomAttribute("department");
- }
-
- public virtual string GetPhone()
- {
- return GetCustomAttribute(ClaimTypes.HomePhone)
- ?? GetCustomAttribute(ClaimTypes.MobilePhone)
- ?? GetCustomAttribute(ClaimTypes.OtherPhone)
- ?? GetCustomAttribute("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/telephonenumber");
- }
-
- public virtual string GetCompany()
- {
- return GetCustomAttribute("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/companyname")
- ?? GetCustomAttribute("organization")
- ?? GetCustomAttribute("User.CompanyName");
- }
-
- public virtual string GetLocation()
- {
- return GetCustomAttribute("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/location")
- ?? GetCustomAttribute("physicalDeliveryOfficeName");
- }
-
- 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?.InnerText;
- }
-
- public string GetCustomAttributeViaFriendlyName(string attr)
- {
- XmlNode node = _xmlDoc.SelectSingleNode("/samlp:Response/saml:Assertion[1]/saml:AttributeStatement/saml:Attribute[@FriendlyName='" + attr + "']/saml:AttributeValue", _xmlNameSpaceManager);
- return node?.InnerText;
- }
-
- public List GetCustomAttributeAsList(string attr)
- {
- XmlNodeList nodes = _xmlDoc.SelectNodes("/samlp:Response/saml:Assertion[1]/saml:AttributeStatement/saml:Attribute[@Name='" + attr + "']/saml:AttributeValue", _xmlNameSpaceManager);
- return nodes?.Cast().Select(x => x.InnerText).ToList();
- }
-
- ///
- /// Decrypts and returns any encrypted attributes using the SAML Service Provider's certificate private key.
- ///
- ///
- /// A list of SAML attribute Name/Value tuples.
- ///
- /// Adapted from: https://github.com/ruialexrib/Programatica.Auth.SAML.ServiceProviderUtils/blob/master/src/Utils/AssertionParserUtils.cs.
- ///
- public IEnumerable<(string Name, string Value)> GetEncryptedAttributes()
- {
- if (_certificate?.HasPrivateKey != true)
- {
- yield break;
- }
-
- var dataElements = _xmlDoc.SelectNodes("/samlp:Response/saml:EncryptedAssertion/xenc:EncryptedData", _xmlNameSpaceManager);
-
- if (dataElements == null || dataElements.Count == 0)
- {
- yield break;
- }
-
- var parserContext = new XmlParserContext(null, _xmlNameSpaceManager, null, XmlSpace.None);
-
- foreach (XmlNode element in dataElements)
- {
- var encryptionAlgorithm = element.SelectSingleNode("//xenc:EncryptionMethod", _xmlNameSpaceManager).Attributes["Algorithm"]?.Value;
- var encryptionKeyAlgorithm = element.SelectSingleNode("//ds:KeyInfo/xenc:EncryptedKey/xenc:EncryptionMethod", _xmlNameSpaceManager)?.Attributes["Algorithm"]?.Value;
- var encryptionKeyCipherValue = element.SelectSingleNode("//ds:KeyInfo/xenc:EncryptedKey/xenc:CipherData/xenc:CipherValue", _xmlNameSpaceManager)?.InnerText;
-
- using var key = Rijndael.Create(encryptionAlgorithm);
- key.Key = EncryptedXml.DecryptKey(
- Convert.FromBase64String(encryptionKeyCipherValue),
- _certificate.GetRSAPrivateKey(),
- useOAEP: encryptionKeyAlgorithm == EncryptedXml.XmlEncRSAOAEPUrl
- );
-
- var encryptedXml = new EncryptedXml();
- var encryptedData = new EncryptedData();
- encryptedData.LoadXml((XmlElement)element);
-
- using var reader = new XmlTextReader(
- Encoding.UTF8.GetString(
- encryptedXml.DecryptData(encryptedData, key)
- ),
- XmlNodeType.Element,
- parserContext);
-
- var attributeElement = XElement.Load(reader);
-
- // Attribute claim type.
- var attributeType = attributeElement.Attribute("Name")?.Value;
-
- // Attribute values.
- foreach (var value in attributeElement.Descendants().Where(e => e?.Name?.LocalName == "AttributeValue"))
- {
- yield return (Name: attributeType, Value: value.Value);
- }
- }
- }
- }
-
- public class SignoutResponse : BaseResponse
- {
- public SignoutResponse(string certificateStr, string responseString = null) : base(certificateStr, responseString) { }
-
- public SignoutResponse(byte[] certificateBytes, string responseString = null) : base(certificateBytes, responseString) { }
-
- public string GetLogoutStatus()
- {
- XmlNode node = _xmlDoc.SelectSingleNode("/samlp:LogoutResponse/samlp:Status/samlp:StatusCode", _xmlNameSpaceManager);
- return node?.Attributes["Value"].Value.Replace("urn:oasis:names:tc:SAML:2.0:status:", string.Empty);
- }
- }
-
- public abstract class BaseRequest
- {
- public string _id;
- protected string _issue_instant;
-
- protected string _issuer;
-
- public BaseRequest(string issuer)
- {
- _id = "_" + Guid.NewGuid().ToString();
- _issue_instant = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ", System.Globalization.CultureInfo.InvariantCulture);
-
- _issuer = issuer;
- }
-
- public abstract string GetRequest();
-
- protected static string ConvertToBase64Deflated(string input)
- {
- //byte[] toEncodeAsBytes = System.Text.ASCIIEncoding.ASCII.GetBytes(input);
- //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();
- using (var writer = new StreamWriter(new DeflateStream(memoryStream, CompressionMode.Compress, true), new UTF8Encoding(false)))
- {
- writer.Write(input);
- writer.Close();
- }
- string result = Convert.ToBase64String(memoryStream.GetBuffer(), 0, (int)memoryStream.Length, Base64FormattingOptions.None);
- return result;
- }
-
- ///
- /// returns the URL you should redirect your users to (i.e. your SAML-provider login URL with the Base64-ed request in the querystring
- ///
- /// SAML provider login url
- /// Optional state to pass through
- ///
- public string GetRedirectUrl(string samlEndpoint, string relayState = null)
- {
- var queryStringSeparator = samlEndpoint.Contains("?") ? "&" : "?";
-
- var url = samlEndpoint + queryStringSeparator + "SAMLRequest=" + Uri.EscapeDataString(GetRequest());
-
- if (!string.IsNullOrEmpty(relayState))
- {
- url += "&RelayState=" + Uri.EscapeDataString(relayState);
- }
-
- return url;
- }
- }
-
- public class AuthRequest : BaseRequest
- {
- private string _assertionConsumerServiceUrl;
-
- ///
- /// Initializes new instance of AuthRequest
- ///
- /// put your EntityID here
- /// put your return URL here
- public AuthRequest(string issuer, string assertionConsumerServiceUrl) : base(issuer)
- {
- _assertionConsumerServiceUrl = assertionConsumerServiceUrl;
- }
-
- ///
- /// get or sets if ForceAuthn attribute is sent to IdP
- ///
- public bool ForceAuthn { get; set; }
-
- [Obsolete("Obsolete, will be removed")]
- public enum AuthRequestFormat
- {
- Base64 = 1
- }
-
- [Obsolete("Obsolete, will be removed, use GetRequest()")]
- public string GetRequest(AuthRequestFormat format) => GetRequest();
-
- ///
- /// returns SAML request as compressed and Base64 encoded XML. You don't need this method
- ///
- ///
- public override string GetRequest()
- {
- using (StringWriter sw = new StringWriter())
- {
- XmlWriterSettings xws = new XmlWriterSettings { 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);
- if (ForceAuthn)
- xw.WriteAttributeString("ForceAuthn", "true");
-
- 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();
- }
-
- return ConvertToBase64Deflated(sw.ToString());
- }
- }
- }
-
- public class SignoutRequest : BaseRequest
- {
- private string _nameId;
-
- public SignoutRequest(string issuer, string nameId) : base(issuer)
- {
- _nameId = nameId;
- }
-
- public override string GetRequest()
- {
- using (StringWriter sw = new StringWriter())
- {
- XmlWriterSettings xws = new XmlWriterSettings { OmitXmlDeclaration = true };
-
- using (XmlWriter xw = XmlWriter.Create(sw, xws))
- {
- xw.WriteStartElement("samlp", "LogoutRequest", "urn:oasis:names:tc:SAML:2.0:protocol");
- xw.WriteAttributeString("ID", _id);
- xw.WriteAttributeString("Version", "2.0");
- xw.WriteAttributeString("IssueInstant", _issue_instant);
-
- xw.WriteStartElement("saml", "Issuer", "urn:oasis:names:tc:SAML:2.0:assertion");
- xw.WriteString(_issuer);
- xw.WriteEndElement();
-
- xw.WriteStartElement("saml", "NameID", "urn:oasis:names:tc:SAML:2.0:assertion");
- xw.WriteString(_nameId);
- xw.WriteEndElement();
-
- xw.WriteEndElement();
- }
-
- return ConvertToBase64Deflated(sw.ToString());
- }
- }
- }
-
- public static class MetaData
- {
- ///
- /// generates XML string describing service provider metadata based on provided EntiytID and Consumer URL
- ///
- ///
- ///
- ///
- public static string Generate(string entityId, string assertionConsumerServiceUrl)
- {
- return $@"
-
-
-
-
- urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified
-
-
-
-";
- }
- }
-}
+/* Jitbit's simple SAML 2.0 component for ASP.NET
+ https://github.com/jitbit/AspNetSaml/
+ (c) Jitbit LP, 2016-2023
+ Use this freely under the Apache license (see https://choosealicense.com/licenses/apache-2.0/)
+*/
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Xml;
+using System.Security.Cryptography.X509Certificates;
+using System.Security.Cryptography.Xml;
+using System.IO.Compression;
+using System.Text;
+using System.Security.Claims;
+using System.Security.Cryptography;
+using System.Xml.Linq;
+
+namespace Saml
+{
+ public abstract class BaseResponse
+ {
+ protected XmlDocument _xmlDoc;
+ protected readonly X509Certificate2 _certificate;
+ protected XmlNamespaceManager _xmlNameSpaceManager; //we need this one to run our XPath queries on the SAML XML
+
+ public string Xml { get { return _xmlDoc.OuterXml; } }
+
+ public BaseResponse(string certificateStr, string responseString = null) : this(Encoding.ASCII.GetBytes(certificateStr), responseString) { }
+
+ public BaseResponse(byte[] certificateBytes, string responseString = null) : this(new X509Certificate2(certificateBytes), responseString) { }
+
+ public BaseResponse(X509Certificate2 certificate, string responseString = null)
+ {
+ _certificate = certificate;
+ if (responseString != null)
+ LoadXmlFromBase64(responseString);
+ }
+
+ ///
+ /// Parse SAML response XML (in case was it not passed in constructor)
+ ///
+ ///
+ /// Creates a default namespace manager if one is not provided.
+ public void LoadXml(string xml, XmlNamespaceManager namespaceManager = null)
+ {
+ _xmlDoc = new XmlDocument { PreserveWhitespace = true, XmlResolver = null };
+ _xmlDoc.LoadXml(xml);
+
+ _xmlNameSpaceManager = namespaceManager ?? GetNamespaceManager(); //lets construct a "manager" for XPath queries
+ }
+
+ public void LoadXmlFromBase64(string response)
+ {
+ UTF8Encoding enc = new UTF8Encoding();
+ LoadXml(enc.GetString(Convert.FromBase64String(response)));
+ }
+
+ //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
+ protected 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 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;
+ }
+
+ return true;
+ }
+
+ //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("xs", "http://www.w3.org/2001/XMLSchema");
+ manager.AddNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance");
+ manager.AddNamespace("ds", SignedXml.XmlDsigNamespaceUrl);
+ manager.AddNamespace("ds", SignedXml.XmlDsigNamespaceUrl);
+ manager.AddNamespace("dsig", SignedXml.XmlDsigNamespaceUrl);
+ manager.AddNamespace("enc", EncryptedXml.XmlEncNamespaceUrl);
+ manager.AddNamespace("xenc", EncryptedXml.XmlEncNamespaceUrl);
+ manager.AddNamespace("xmlenc", EncryptedXml.XmlEncNamespaceUrl);
+ manager.AddNamespace("saml", "urn:oasis:names:tc:SAML:2.0:assertion");
+ manager.AddNamespace("samlp", "urn:oasis:names:tc:SAML:2.0:protocol");
+
+ return manager;
+ }
+
+ ///
+ /// Checks the validity of SAML response (validate signature, check expiration date etc)
+ ///
+ ///
+ public bool IsValid()
+ {
+ XmlNodeList nodeList = _xmlDoc.SelectNodes("//ds:Signature", _xmlNameSpaceManager);
+
+ SignedXml signedXml = new SignedXml(_xmlDoc);
+
+ if (nodeList.Count == 0) return false;
+
+ signedXml.LoadXml((XmlElement)nodeList[0]);
+ return ValidateSignatureReference(signedXml) &&
+ signedXml.CheckSignature(_certificate, true) &&
+ !IsExpired();
+ }
+
+ 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();
+ }
+ }
+
+ public class Response : BaseResponse
+ {
+ public Response(string certificateStr, string responseString = null) : base(certificateStr, responseString) { }
+
+ public Response(byte[] certificateBytes, string responseString = null) : base(certificateBytes, responseString) { }
+
+ public Response(X509Certificate2 certificate, string responseString = null) : base(certificate, responseString) { }
+
+ ///
+ /// returns the User's login
+ ///
+ public string GetNameID()
+ {
+ XmlNode node = _xmlDoc.SelectSingleNode("/samlp:Response/saml:Assertion[1]/saml:Subject/saml:NameID", _xmlNameSpaceManager);
+ return node.InnerText;
+ }
+
+ public virtual string GetUpn()
+ {
+ return GetCustomAttribute(ClaimTypes.Upn);
+ }
+
+ public virtual string GetEmail()
+ {
+ return GetCustomAttribute("User.email")
+ ?? GetCustomAttribute(ClaimTypes.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("mail") //some providers put last name into an attribute named "mail"
+ ?? GetCustomAttribute("email"); //some providers put last name into an attribute named "email"
+ }
+
+ public virtual string GetFirstName()
+ {
+ return GetCustomAttribute("first_name")
+ ?? GetCustomAttribute(ClaimTypes.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 GetLastName()
+ {
+ return GetCustomAttribute("last_name")
+ ?? GetCustomAttribute(ClaimTypes.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 GetDepartment()
+ {
+ return GetCustomAttribute("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/department")
+ ?? GetCustomAttribute("department");
+ }
+
+ public virtual string GetPhone()
+ {
+ return GetCustomAttribute(ClaimTypes.HomePhone)
+ ?? GetCustomAttribute(ClaimTypes.MobilePhone)
+ ?? GetCustomAttribute(ClaimTypes.OtherPhone)
+ ?? GetCustomAttribute("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/telephonenumber");
+ }
+
+ public virtual string GetCompany()
+ {
+ return GetCustomAttribute("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/companyname")
+ ?? GetCustomAttribute("organization")
+ ?? GetCustomAttribute("User.CompanyName");
+ }
+
+ public virtual string GetLocation()
+ {
+ return GetCustomAttribute("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/location")
+ ?? GetCustomAttribute("physicalDeliveryOfficeName");
+ }
+
+ 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?.InnerText;
+ }
+
+ public string GetCustomAttributeViaFriendlyName(string attr)
+ {
+ XmlNode node = _xmlDoc.SelectSingleNode("/samlp:Response/saml:Assertion[1]/saml:AttributeStatement/saml:Attribute[@FriendlyName='" + attr + "']/saml:AttributeValue", _xmlNameSpaceManager);
+ return node?.InnerText;
+ }
+
+ public List GetCustomAttributeAsList(string attr)
+ {
+ XmlNodeList nodes = _xmlDoc.SelectNodes("/samlp:Response/saml:Assertion[1]/saml:AttributeStatement/saml:Attribute[@Name='" + attr + "']/saml:AttributeValue", _xmlNameSpaceManager);
+ return nodes?.Cast().Select(x => x.InnerText).ToList();
+ }
+
+ ///
+ /// Decrypts and returns any encrypted attributes using the SAML Service Provider's certificate private key.
+ ///
+ ///
+ /// A list of SAML attribute Name/Value tuples.
+ ///
+ /// Adapted from: https://github.com/ruialexrib/Programatica.Auth.SAML.ServiceProviderUtils/blob/master/src/Utils/AssertionParserUtils.cs.
+ ///
+ public IEnumerable<(string Name, string Value)> GetEncryptedAttributes()
+ {
+ if (_certificate?.HasPrivateKey != true)
+ {
+ yield break;
+ }
+
+ var dataElements = _xmlDoc.SelectNodes("/samlp:Response/saml:EncryptedAssertion/xenc:EncryptedData", _xmlNameSpaceManager);
+
+ if (dataElements == null || dataElements.Count == 0)
+ {
+ yield break;
+ }
+
+ var parserContext = new XmlParserContext(null, _xmlNameSpaceManager, null, XmlSpace.None);
+
+ foreach (XmlNode element in dataElements)
+ {
+ var encryptionAlgorithm = element.SelectSingleNode("//xenc:EncryptionMethod", _xmlNameSpaceManager).Attributes["Algorithm"]?.Value;
+ var encryptionKeyAlgorithm = element.SelectSingleNode("//ds:KeyInfo/xenc:EncryptedKey/xenc:EncryptionMethod", _xmlNameSpaceManager)?.Attributes["Algorithm"]?.Value;
+ var encryptionKeyCipherValue = element.SelectSingleNode("//ds:KeyInfo/xenc:EncryptedKey/xenc:CipherData/xenc:CipherValue", _xmlNameSpaceManager)?.InnerText;
+
+ using var key = Rijndael.Create(encryptionAlgorithm);
+ key.Key = EncryptedXml.DecryptKey(
+ Convert.FromBase64String(encryptionKeyCipherValue),
+ _certificate.GetRSAPrivateKey(),
+ useOAEP: encryptionKeyAlgorithm == EncryptedXml.XmlEncRSAOAEPUrl
+ );
+
+ var encryptedXml = new EncryptedXml();
+ var encryptedData = new EncryptedData();
+ encryptedData.LoadXml((XmlElement)element);
+
+ using var reader = new XmlTextReader(
+ Encoding.UTF8.GetString(
+ encryptedXml.DecryptData(encryptedData, key)
+ ),
+ XmlNodeType.Element,
+ parserContext);
+
+ var attributeElement = XElement.Load(reader);
+
+ // Attribute claim type.
+ var attributeType = attributeElement.Attribute("Name")?.Value;
+
+ // Attribute values.
+ foreach (var value in attributeElement.Descendants().Where(e => e?.Name?.LocalName == "AttributeValue"))
+ {
+ yield return (Name: attributeType, Value: value.Value);
+ }
+ }
+ }
+ }
+
+ public class SignoutResponse : BaseResponse
+ {
+ public SignoutResponse(string certificateStr, string responseString = null) : base(certificateStr, responseString) { }
+
+ public SignoutResponse(byte[] certificateBytes, string responseString = null) : base(certificateBytes, responseString) { }
+
+ public string GetLogoutStatus()
+ {
+ XmlNode node = _xmlDoc.SelectSingleNode("/samlp:LogoutResponse/samlp:Status/samlp:StatusCode", _xmlNameSpaceManager);
+ return node?.Attributes["Value"].Value.Replace("urn:oasis:names:tc:SAML:2.0:status:", string.Empty);
+ }
+ }
+
+ public abstract class BaseRequest
+ {
+ public string _id;
+ protected string _issue_instant;
+
+ protected string _issuer;
+
+ public BaseRequest(string issuer)
+ {
+ _id = "_" + Guid.NewGuid().ToString();
+ _issue_instant = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ", System.Globalization.CultureInfo.InvariantCulture);
+
+ _issuer = issuer;
+ }
+
+ public abstract string GetRequest();
+
+ protected static string ConvertToBase64Deflated(string input)
+ {
+ //byte[] toEncodeAsBytes = System.Text.ASCIIEncoding.ASCII.GetBytes(input);
+ //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();
+ using (var writer = new StreamWriter(new DeflateStream(memoryStream, CompressionMode.Compress, true), new UTF8Encoding(false)))
+ {
+ writer.Write(input);
+ writer.Close();
+ }
+ string result = Convert.ToBase64String(memoryStream.GetBuffer(), 0, (int)memoryStream.Length, Base64FormattingOptions.None);
+ return result;
+ }
+
+ ///
+ /// returns the URL you should redirect your users to (i.e. your SAML-provider login URL with the Base64-ed request in the querystring
+ ///
+ /// SAML provider login url
+ /// Optional state to pass through
+ ///
+ public string GetRedirectUrl(string samlEndpoint, string relayState = null)
+ {
+ var queryStringSeparator = samlEndpoint.Contains("?") ? "&" : "?";
+
+ var url = samlEndpoint + queryStringSeparator + "SAMLRequest=" + Uri.EscapeDataString(GetRequest());
+
+ if (!string.IsNullOrEmpty(relayState))
+ {
+ url += "&RelayState=" + Uri.EscapeDataString(relayState);
+ }
+
+ return url;
+ }
+ }
+
+ public class AuthRequest : BaseRequest
+ {
+ private string _assertionConsumerServiceUrl;
+
+ ///
+ /// Initializes new instance of AuthRequest
+ ///
+ /// put your EntityID here
+ /// put your return URL here
+ public AuthRequest(string issuer, string assertionConsumerServiceUrl) : base(issuer)
+ {
+ _assertionConsumerServiceUrl = assertionConsumerServiceUrl;
+ }
+
+ ///
+ /// get or sets if ForceAuthn attribute is sent to IdP
+ ///
+ public bool ForceAuthn { get; set; }
+
+ [Obsolete("Obsolete, will be removed")]
+ public enum AuthRequestFormat
+ {
+ Base64 = 1
+ }
+
+ [Obsolete("Obsolete, will be removed, use GetRequest()")]
+ public string GetRequest(AuthRequestFormat format) => GetRequest();
+
+ ///
+ /// returns SAML request as compressed and Base64 encoded XML. You don't need this method
+ ///
+ ///
+ public override string GetRequest()
+ {
+ using (StringWriter sw = new StringWriter())
+ {
+ XmlWriterSettings xws = new XmlWriterSettings { 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);
+ if (ForceAuthn)
+ xw.WriteAttributeString("ForceAuthn", "true");
+
+ 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();
+ }
+
+ return ConvertToBase64Deflated(sw.ToString());
+ }
+ }
+ }
+
+ public class SignoutRequest : BaseRequest
+ {
+ private string _nameId;
+
+ public SignoutRequest(string issuer, string nameId) : base(issuer)
+ {
+ _nameId = nameId;
+ }
+
+ public override string GetRequest()
+ {
+ using (StringWriter sw = new StringWriter())
+ {
+ XmlWriterSettings xws = new XmlWriterSettings { OmitXmlDeclaration = true };
+
+ using (XmlWriter xw = XmlWriter.Create(sw, xws))
+ {
+ xw.WriteStartElement("samlp", "LogoutRequest", "urn:oasis:names:tc:SAML:2.0:protocol");
+ xw.WriteAttributeString("ID", _id);
+ xw.WriteAttributeString("Version", "2.0");
+ xw.WriteAttributeString("IssueInstant", _issue_instant);
+
+ xw.WriteStartElement("saml", "Issuer", "urn:oasis:names:tc:SAML:2.0:assertion");
+ xw.WriteString(_issuer);
+ xw.WriteEndElement();
+
+ xw.WriteStartElement("saml", "NameID", "urn:oasis:names:tc:SAML:2.0:assertion");
+ xw.WriteString(_nameId);
+ xw.WriteEndElement();
+
+ xw.WriteEndElement();
+ }
+
+ return ConvertToBase64Deflated(sw.ToString());
+ }
+ }
+ }
+
+ public static class MetaData
+ {
+ ///
+ /// generates XML string describing service provider metadata based on provided EntiytID and Consumer URL
+ ///
+ ///
+ ///
+ ///
+ public static string Generate(string entityId, string assertionConsumerServiceUrl)
+ {
+ return $@"
+
+
+
+
+ urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified
+
+
+
+";
+ }
+ }
+}