diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..a0730ef
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,103 @@
+root = true
+
+[*]
+end_of_line = lf
+insert_final_newline = true
+indent_style = tab
+indent_size = 4
+tab_width = 4
+charset = Windows-1252 # Western European Windows
+
+[*.cs]
+indent_style = tab
+indent_size = 4
+charset = Windows-1252 # Western European Windows
+csharp_indent_labels = one_less_than_current
+csharp_using_directive_placement = outside_namespace:silent
+csharp_prefer_simple_using_statement = true:suggestion
+csharp_prefer_braces = true:silent
+csharp_style_namespace_declarations = block_scoped:silent
+csharp_style_prefer_method_group_conversion = true:silent
+csharp_style_prefer_top_level_statements = true:silent
+csharp_style_prefer_primary_constructors = true:suggestion
+csharp_style_expression_bodied_methods = false:silent
+csharp_style_expression_bodied_constructors = false:silent
+csharp_style_expression_bodied_operators = false:silent
+csharp_style_expression_bodied_properties = true:silent
+csharp_style_expression_bodied_indexers = true:silent
+csharp_style_expression_bodied_accessors = true:silent
+csharp_style_expression_bodied_lambdas = true:silent
+csharp_style_expression_bodied_local_functions = false:silent
+csharp_style_throw_expression = true:suggestion
+csharp_style_prefer_null_check_over_type_check = true:suggestion
+csharp_prefer_simple_default_expression = true:suggestion
+csharp_style_prefer_local_over_anonymous_function = true:suggestion
+csharp_style_prefer_index_operator = true:suggestion
+
+[*.{cs,vb}]
+#### Naming styles ####
+
+# Naming rules
+
+dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion
+dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
+dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
+
+dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion
+dotnet_naming_rule.types_should_be_pascal_case.symbols = types
+dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case
+
+dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion
+dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
+dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
+
+# Symbol specifications
+
+dotnet_naming_symbols.interface.applicable_kinds = interface
+dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.interface.required_modifiers =
+
+dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
+dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.types.required_modifiers =
+
+dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
+dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.non_field_members.required_modifiers =
+
+# Naming styles
+
+dotnet_naming_style.begins_with_i.required_prefix = I
+dotnet_naming_style.begins_with_i.required_suffix =
+dotnet_naming_style.begins_with_i.word_separator =
+dotnet_naming_style.begins_with_i.capitalization = pascal_case
+
+dotnet_naming_style.pascal_case.required_prefix =
+dotnet_naming_style.pascal_case.required_suffix =
+dotnet_naming_style.pascal_case.word_separator =
+dotnet_naming_style.pascal_case.capitalization = pascal_case
+
+dotnet_naming_style.pascal_case.required_prefix =
+dotnet_naming_style.pascal_case.required_suffix =
+dotnet_naming_style.pascal_case.word_separator =
+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 = 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
+dotnet_style_prefer_auto_properties = true:silent
+dotnet_style_object_initializer = true:suggestion
+dotnet_style_collection_initializer = true:suggestion
+dotnet_style_prefer_simplified_boolean_expressions = true:suggestion
+dotnet_style_prefer_conditional_expression_over_assignment = true:silent
+dotnet_style_prefer_conditional_expression_over_return = true:silent
+dotnet_style_explicit_tuple_names = true:suggestion
+dotnet_style_prefer_inferred_tuple_names = true:suggestion
+dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
+dotnet_style_prefer_compound_assignment = true:suggestion
+dotnet_style_prefer_simplified_interpolation = true:suggestion
+dotnet_style_namespace_match_folder = true:suggestion
+indent_style = tab
diff --git a/AspNetSaml.Tests/AspNetSaml.Tests.csproj b/AspNetSaml.Tests/AspNetSaml.Tests.csproj
index 855f81a..7205bff 100644
--- a/AspNetSaml.Tests/AspNetSaml.Tests.csproj
+++ b/AspNetSaml.Tests/AspNetSaml.Tests.csproj
@@ -13,6 +13,7 @@
+
diff --git a/AspNetSaml.Tests/Constants.cs b/AspNetSaml.Tests/Constants.cs
new file mode 100644
index 0000000..41cf21c
--- /dev/null
+++ b/AspNetSaml.Tests/Constants.cs
@@ -0,0 +1,107 @@
+using System.Security.Cryptography.X509Certificates;
+
+namespace AspNetSaml.Tests;
+
+public static class Constants
+{
+ ///
+ /// Test certificate values.
+ ///
+ ///
+ /// Self-signed certificates generated by https://www.samltool.com/self_signed_certs.php.
+ ///
+ public static class Certificates
+ {
+ public const string Country = "US";
+ public const string State = "New York";
+ public const string Locality = "New York City";
+ public const string Organization = "AspNetSaml";
+ public const string Domain = "aspnetsaml.jitbit.local";
+ public const string DigestAlgorithm = "SHA512";
+
+ ///
+ /// Private key raw text.
+ ///
+ public const string PrivateKey = @"-----BEGIN PRIVATE KEY-----
+MIIEwgIBADANBgkqhkiG9w0BAQEFAASCBKwwggSoAgEAAoIBAgDb+eVLX3/pcYbX
+gustW1YSSTIe737KuJqL9CxibjL2jaEXvoM0zllwYdyvWdrnoJ8tABoKHPtSGRJv
+6fH7+cq31zLj50R6Wz4uzdZr37opBdk5ea0YHeaOOmNu1ikfFNMaT0VXuj9kqod8
+V/N0Qv77eDaixOivV2nnXGxrgYh/m9dKrr5bdTpZ46FsSMOpgYpI6LXWfI7dNBFZ
+uyVKClGTtcbJmONo8NEPR+ELgNltgLacvMHzZECavsqge42RDmfcBCFOr3AdiLSR
+85DANZxZ5sRCMaZHEz+5DhL/x+MG5UVF9h7DlvgJMz1Ygvd1EAvbhR0ifQQDyQW6
+XJtH3bFiswIDAQABAoIBAgDRvSBCUIkudP9DhuFjer3Da6TtWB8FfSRmIuca5sWS
+zZF2iUCi3cjrXXPEgaE1zrFWf81ULTP3oE4zBNWkEhSWWwp7wGtLWqociEhUzJm8
+OYZXxcsjvoawv71E1c+ZggqSAFk2fy+odOv/xAAtrx9dd85oPeU6IdepMDdz/aq/
+OSSeRqTgIRSZ95mQYPx1wE5t/FCQqY3KSpkagO46SIzIGUwuLDTBhQTJTyGesME8
+NvZ4HR1l9/6uFyESru31bEHgL0jY0mFM7zf+d/pkKaerXQOieW+GFrH5D697sR3E
+QMou6OzAtdMIAmwX7W6NHw55dlITHbT27HD1riEWRol+qQKBgQ/259ywfKhX5a1l
+4kVQ/uVshSQX4DxdHmEFC9WXudH0OeEXrJKyufxhCqbIl3SSXwzOCO4wK+/qwtgP
+Yl7YbkOQQcTibrH9DMO7Zyb01R1HdJYYA68L2EOLRcJKq7+RVGocrzopRxlRU2gJ
+n8tgKos5jRwur/fadLKGe10MRPKipQKBgQ3Hc08oO39kOK3umoUm1TYRiuSKw7Uh
+91Vml44FZTp0Aw84AeTJEZ4pl3T6apaoTh2ONAxihCpuQUtVhJEKREj4GdD/lDDi
++dmA8eOGDMDY0R/4pOwFLgOhy2CibQIsxljPD0if9IDIP2NGYl+WziCjIQcDjvBb
+jREfwGyWRyYodwKBgQkcVjwq+Ck2aFvprhTy4VTa9qyfd5fbaI/jylouCZzZLQLZ
+eOILX3q5gtOl3FFpixcKqiwMj7aOmn2lYfVQvLSQKgiLVLL9AADf/UFNLiZUdiOG
+Nuv57YS2gawc4yEjdjJMhm/ByNKZB+lyvJ/bFMx5np87wa7IHBsaBmMWsm5qBQKB
+gQsjsU7PIbqNVV0Xxofaqwe5CuZUYH9w5DmAZQmFxx6IZ2jISI+jFcEdsrn5MG53
+xh8StXVFt79tvw+eJTv0Ztvu50AVPsJ+3KpAGk1sM6c8IWSNaRb94QNCq96FsUbO
+19M4Igz+c3YhbU1eu2y3yBCOkMbQ05/xA4xSdQfUPdTVZQKBgQls0hLLjqJf7ag7
+rbw2gP9e72jwXdO+FyChohY5TNv3081azT/pHdNaAQset1dCBtibhtwjO6HMwXEB
+FB1gREpjQSw0rs9n/MLm0ASWVftfn84QX0Uanpz7/Ma18x7TBUX21kZusWCtVjUG
+XY1SLPWC2KRvi85oYPvpNFI1NKUD1g==
+-----END PRIVATE KEY-----";
+
+ ///
+ /// Certificate raw text.
+ ///
+ public const string PublicCertificate = @"-----BEGIN CERTIFICATE-----
+MIIDzzCCAragAwIBAgIBADANBgkqhkiG9w0BAQ0FADCBgDELMAkGA1UEBhMCdXMx
+ETAPBgNVBAgMCE5ldyBZb3JrMQ8wDQYDVQQKDAZKaXRiaXQxIDAeBgNVBAMMF2Fz
+cG5ldHNhbWwuaml0Yml0LmxvY2FsMRYwFAYDVQQHDA1OZXcgWW9yayBDaXR5MRMw
+EQYDVQQLDApBc3BOZXRTYW1sMB4XDTIzMDgyNjE1MTkwMFoXDTMzMDgyMzE1MTkw
+MFowgYAxCzAJBgNVBAYTAnVzMREwDwYDVQQIDAhOZXcgWW9yazEPMA0GA1UECgwG
+Sml0Yml0MSAwHgYDVQQDDBdhc3BuZXRzYW1sLmppdGJpdC5sb2NhbDEWMBQGA1UE
+BwwNTmV3IFlvcmsgQ2l0eTETMBEGA1UECwwKQXNwTmV0U2FtbDCCASMwDQYJKoZI
+hvcNAQEBBQADggEQADCCAQsCggECANv55Utff+lxhteC6y1bVhJJMh7vfsq4mov0
+LGJuMvaNoRe+gzTOWXBh3K9Z2uegny0AGgoc+1IZEm/p8fv5yrfXMuPnRHpbPi7N
+1mvfuikF2Tl5rRgd5o46Y27WKR8U0xpPRVe6P2Sqh3xX83RC/vt4NqLE6K9Xaedc
+bGuBiH+b10quvlt1OlnjoWxIw6mBikjotdZ8jt00EVm7JUoKUZO1xsmY42jw0Q9H
+4QuA2W2Atpy8wfNkQJq+yqB7jZEOZ9wEIU6vcB2ItJHzkMA1nFnmxEIxpkcTP7kO
+Ev/H4wblRUX2HsOW+AkzPViC93UQC9uFHSJ9BAPJBbpcm0fdsWKzAgMBAAGjUDBO
+MB0GA1UdDgQWBBSyDqBxnYWoWDEO/KM7qBRzpmrMfTAfBgNVHSMEGDAWgBSyDqBx
+nYWoWDEO/KM7qBRzpmrMfTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBDQUAA4IB
+AgBXHSnFy9CiwEWMf1/AECqJxUqYZAl/e4Hso5gN8z/VVFlElHh5/gvDRVZMiIel
+GBTfihwE7C2ftbD5u9RDAsaktkEseL/QDDYqJScwtosYxMgZLaINdXilkyi9xc72
+6akVo+xx/qCnZYAf4Cs8k+WZXvn6rjUmjgrzFHCAlPvXp2PCyCHS2PFcAmkKHr2V
+EcEnHvJi/ujia9gMF8dlbOw+Brbl8KcQ8IVinHB3/C8Op4lynoMFdrv6boDFHEyh
+p3Jm5xUMH1/ow3qJ+Ffv2chCD0R6RPUXbhNUixZPuPRECbW0TDp+GgDtKCNMuB0m
+ugfn/Qef81oPEImyoMWd0ReQvA==
+-----END CERTIFICATE-----";
+
+ ///
+ /// CSR raw text.
+ ///
+ public const string CertificateSigningRequest = @"-----BEGIN CERTIFICATE REQUEST-----
+MIICyDCCAa8CAQAwgYAxCzAJBgNVBAYTAnVzMREwDwYDVQQIDAhOZXcgWW9yazEP
+MA0GA1UECgwGSml0Yml0MSAwHgYDVQQDDBdhc3BuZXRzYW1sLmppdGJpdC5sb2Nh
+bDEWMBQGA1UEBwwNTmV3IFlvcmsgQ2l0eTETMBEGA1UECwwKQXNwTmV0U2FtbDCC
+ASMwDQYJKoZIhvcNAQEBBQADggEQADCCAQsCggECANv55Utff+lxhteC6y1bVhJJ
+Mh7vfsq4mov0LGJuMvaNoRe+gzTOWXBh3K9Z2uegny0AGgoc+1IZEm/p8fv5yrfX
+MuPnRHpbPi7N1mvfuikF2Tl5rRgd5o46Y27WKR8U0xpPRVe6P2Sqh3xX83RC/vt4
+NqLE6K9XaedcbGuBiH+b10quvlt1OlnjoWxIw6mBikjotdZ8jt00EVm7JUoKUZO1
+xsmY42jw0Q9H4QuA2W2Atpy8wfNkQJq+yqB7jZEOZ9wEIU6vcB2ItJHzkMA1nFnm
+xEIxpkcTP7kOEv/H4wblRUX2HsOW+AkzPViC93UQC9uFHSJ9BAPJBbpcm0fdsWKz
+AgMBAAGgADANBgkqhkiG9w0BAQ0FAAOCAQIAcs7QprOVI+4Q8c6A/xlEqHzYJte3
+mdkCDmZsuO8FH6oRY0fybPv4NAziTGnWg7s7sabAMvnE79zOhPT6/LqSxvofMj0t
+lbO62COVMT1NUGYCnb7346oouvscIZusey8olEIf8EwiQTmCm7ait7MrnA9Mi0Tc
+VExbYdfBZZcOFMZJmG4bQz+G66SEdBemlyGrdMxDR9+pwgBOG49wDOfyIk6rvkG3
+A3ZafNtw5AXMLEWegWs6AEH1tkRMrJgIX9jQwP7mu3RocRzkrbSz2juzlLgyLDPn
+t9RbkNHiT5rwOlSe3b86oonejHDEj4RDVNr89/6LJ3KxAsL5bUGCJaQbhWU=
+-----END CERTIFICATE REQUEST-----";
+
+ ///
+ /// Test certificate instance.
+ ///
+ public static X509Certificate2 Certificate => new Lazy(() => X509Certificate2.CreateFromPem(PublicCertificate, PrivateKey)).Value;
+ }
+}
diff --git a/AspNetSaml.Tests/UnitTests.cs b/AspNetSaml.Tests/UnitTests.cs
index 46ef4a9..66290d4 100644
--- a/AspNetSaml.Tests/UnitTests.cs
+++ b/AspNetSaml.Tests/UnitTests.cs
@@ -1,24 +1,25 @@
using Saml;
using System.IO.Compression;
-using System.IO;
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-----
+ [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(@"
+ var samlresp = new Saml.Response(cert);
+ samlresp.LoadXml(@"
http://idp.example.com/metadata.php
@@ -61,88 +62,152 @@ public void TestSamlResponseValidator()
");
- Assert.IsTrue(samlresp.IsValid());
- Assert.IsTrue(samlresp.GetNameID() == "_ce3d2948b4cf20146dee0a0b3dd6f69b6cf86f62d7");
+ samlresp.IsValid().ShouldBeTrue();
+ samlresp.GetNameID().ShouldBe("_ce3d2948b4cf20146dee0a0b3dd6f69b6cf86f62d7");
+ samlresp.GetEmail().ShouldBe("test@example.com");
+ samlresp.GetCustomAttribute("uid").ShouldBe("test");
+ }
- Assert.IsTrue(samlresp.GetEmail() == "test@example.com");
+ [TestMethod]
+ public void TestSamlSignoutResponseValidator()
+ {
+ //this test's cert and signature borrowed from https://github.com/boxyhq/jackson/
- Assert.IsTrue(samlresp.GetCustomAttribute("uid") == "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=");
- Assert.IsTrue(samlresp.IsValid());
-
- Assert.IsTrue(samlresp.GetLogoutStatus() == "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");
-
- Assert.IsTrue(samlresp.IsValid());
+ 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=");
+
- Assert.IsTrue(samlresp.GetCustomAttributeViaFriendlyName("givenName") == "Guest");
-
- Assert.IsTrue(Enumerable.SequenceEqual(samlresp.GetCustomAttributeAsList("Role"), new List { "uma_authorization", "offline_access", "default-roles-poc", "view-profile", "manage-account", "manage-account-links", "SimpleUser" }));
- }
+ samlresp.IsValid().ShouldBeTrue();
+ samlresp.GetLogoutStatus().ShouldBe("Success");
+ }
- [TestMethod]
- public void TestSamlRequest()
- {
- var samlEndpoint = "http://saml-provider-that-we-use.com/login/";
+ [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();
+ 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();
+ //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());
-
- Assert.IsTrue(str.EndsWith(@"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"));
+ //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-----
+ [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);
- Assert.IsTrue(x.SequenceEqual(y));
- }
-
+-----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];
@@ -151,6 +216,6 @@ private static byte[] StringToByteArray(string st)
bytes[i] = (byte)st[i];
}
return bytes;
- }
+ }
}
-}
\ No newline at end of file
+}
diff --git a/AspNetSaml.Tests/Usings.cs b/AspNetSaml.Tests/Usings.cs
index ab67c7e..540383d 100644
--- a/AspNetSaml.Tests/Usings.cs
+++ b/AspNetSaml.Tests/Usings.cs
@@ -1 +1 @@
-global using Microsoft.VisualStudio.TestTools.UnitTesting;
\ No newline at end of file
+global using Microsoft.VisualStudio.TestTools.UnitTesting;
diff --git a/AspNetSaml/AspNetSaml.csproj b/AspNetSaml/AspNetSaml.csproj
index 5999ae4..d206f98 100644
--- a/AspNetSaml/AspNetSaml.csproj
+++ b/AspNetSaml/AspNetSaml.csproj
@@ -1,6 +1,7 @@
+ latest
netstandard2.0
AspNetSaml
AspNetSaml
diff --git a/AspNetSaml/Saml.cs b/AspNetSaml/Saml.cs
index 6ba205c..cc0375a 100644
--- a/AspNetSaml/Saml.cs
+++ b/AspNetSaml/Saml.cs
@@ -13,6 +13,9 @@ Use this freely under the Apache license (see https://choosealicense.com/license
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
{
@@ -20,42 +23,46 @@ 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
-
+ 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)
+ public BaseResponse(byte[] certificateBytes, string responseString = null) : this(new X509Certificate2(certificateBytes), responseString) { }
+
+ public BaseResponse(X509Certificate2 certificate, string responseString = null)
{
- _certificate = new X509Certificate2(certificateBytes);
+ _certificate = certificate;
if (responseString != null)
LoadXmlFromBase64(responseString);
}
-
+
///
/// Parse SAML response XML (in case was it not passed in constructor)
///
- public void LoadXml(string xml)
+ ///
+ /// 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 = GetNamespaceManager(); //lets construct a "manager" for XPath queries
+ _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
+ }
+
+ //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
+ if (signedXml.SignedInfo.References.Count != 1) //no ref at all
return false;
var reference = (Reference)signedXml.SignedInfo.References[0];
@@ -65,7 +72,7 @@ protected bool ValidateSignatureReference(SignedXml signedXml)
if (idElement == _xmlDoc.DocumentElement)
return true;
- else //sometimes its not the "root" doc-element that is being signed, but the "assertion" element
+ 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)
@@ -73,20 +80,28 @@ protected bool ValidateSignatureReference(SignedXml signedXml)
}
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
+ }
+
+ //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);
+ 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)
///
@@ -100,7 +115,9 @@ public bool IsValid()
if (nodeList.Count == 0) return false;
signedXml.LoadXml((XmlElement)nodeList[0]);
- return ValidateSignatureReference(signedXml) && signedXml.CheckSignature(_certificate, true) && !IsExpired();
+ return ValidateSignatureReference(signedXml) &&
+ signedXml.CheckSignature(_certificate, true) &&
+ !IsExpired();
}
private bool IsExpired()
@@ -112,7 +129,7 @@ private bool IsExpired()
DateTime.TryParse(node.Attributes["NotOnOrAfter"].Value, out expirationDate);
}
return DateTime.UtcNow > expirationDate.ToUniversalTime();
- }
+ }
}
public class Response : BaseResponse
@@ -121,6 +138,8 @@ public Response(string certificateStr, string responseString = null) : base(cert
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
///
@@ -132,20 +151,21 @@ public string GetNameID()
public virtual string GetUpn()
{
- return GetCustomAttribute("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn");
+ return GetCustomAttribute(ClaimTypes.Upn);
}
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"
+ ?? 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("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(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"
}
@@ -153,7 +173,7 @@ public virtual string GetFirstName()
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(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"
}
@@ -166,7 +186,9 @@ public virtual string GetDepartment()
public virtual string GetPhone()
{
- return GetCustomAttribute("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/homephone")
+ return GetCustomAttribute(ClaimTypes.HomePhone)
+ ?? GetCustomAttribute(ClaimTypes.MobilePhone)
+ ?? GetCustomAttribute(ClaimTypes.OtherPhone)
?? GetCustomAttribute("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/telephonenumber");
}
@@ -186,19 +208,80 @@ public virtual string GetLocation()
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();
+ 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);
+ }
+ }
}
}
@@ -232,22 +315,22 @@ public BaseRequest(string 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;
+ 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
///
@@ -260,7 +343,7 @@ public string GetRedirectUrl(string samlEndpoint, string relayState = null)
var url = samlEndpoint + queryStringSeparator + "SAMLRequest=" + Uri.EscapeDataString(GetRequest());
- if (!string.IsNullOrEmpty(relayState))
+ if (!string.IsNullOrEmpty(relayState))
{
url += "&RelayState=" + Uri.EscapeDataString(relayState);
}
@@ -269,34 +352,34 @@ public string GetRedirectUrl(string samlEndpoint, string relayState = null)
}
}
- public class AuthRequest : BaseRequest
- {
- private string _assertionConsumerServiceUrl;
-
- ///
- /// Initializes new instance of AuthRequest
- ///
- /// put your EntityID here
- /// put your return URL here
+ 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 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();
-
+ }
+
+ [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
///
@@ -325,21 +408,21 @@ public override string GetRequest()
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.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();*/
+
xw.WriteEndElement();
- }
-
+ }
+
return ConvertToBase64Deflated(sw.ToString());
}
- }
+ }
}
public class SignoutRequest : BaseRequest
@@ -380,30 +463,30 @@ public override string GetRequest()
}
}
- 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
-
-
-
-";
- }
+ 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
+
+
+
+";
+ }
}
-}
\ No newline at end of file
+}