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 +}