From 25cf07b5f225c462874357862285a099cce3ee97 Mon Sep 17 00:00:00 2001 From: Havagan Date: Sat, 26 Aug 2023 13:08:36 -0400 Subject: [PATCH] #35 - Added support for encrypted assertions. --- AspNetSaml.Tests/AspNetSaml.Tests.csproj | 1 + AspNetSaml.Tests/Constants.cs | 107 +++ AspNetSaml.Tests/UnitTests.cs | 341 +++++---- AspNetSaml/AspNetSaml.csproj | 1 + AspNetSaml/Saml.cs | 867 +++++++++++++---------- 5 files changed, 787 insertions(+), 530 deletions(-) create mode 100644 AspNetSaml.Tests/Constants.cs 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..561ca4d --- /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..52918bb 100644 --- a/AspNetSaml.Tests/UnitTests.cs +++ b/AspNetSaml.Tests/UnitTests.cs @@ -1,156 +1,221 @@ -using Saml; -using System.IO.Compression; -using System.IO; -using System.Text; - -namespace AspNetSaml.Tests -{ - [TestClass] - public class UnitTests - { - //cert and signature taken form here: www.samltool.com/generic_sso_res.php - - [TestMethod] - public void TestSamlResponseValidator() - { - var cert = @"-----BEGIN CERTIFICATE----- -MIICajCCAdOgAwIBAgIBADANBgkqhkiG9w0BAQ0FADBSMQswCQYDVQQGEwJ1czETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMT25lbG9naW4gSW5jMRcwFQYDVQQDDA5zcC5leGFtcGxlLmNvbTAeFw0xNDA3MTcxNDEyNTZaFw0xNTA3MTcxNDEyNTZaMFIxCzAJBgNVBAYTAnVzMRMwEQYDVQQIDApDYWxpZm9ybmlhMRUwEwYDVQQKDAxPbmVsb2dpbiBJbmMxFzAVBgNVBAMMDnNwLmV4YW1wbGUuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDZx+ON4IUoIWxgukTb1tOiX3bMYzYQiwWPUNMp+Fq82xoNogso2bykZG0yiJm5o8zv/sd6pGouayMgkx/2FSOdc36T0jGbCHuRSbtia0PEzNIRtmViMrt3AeoWBidRXmZsxCNLwgIV6dn2WpuE5Az0bHgpZnQxTKFek0BMKU/d8wIDAQABo1AwTjAdBgNVHQ4EFgQUGHxYqZYyX7cTxKVODVgZwSTdCnwwHwYDVR0jBBgwFoAUGHxYqZYyX7cTxKVODVgZwSTdCnwwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQ0FAAOBgQByFOl+hMFICbd3DJfnp2Rgd/dqttsZG/tyhILWvErbio/DEe98mXpowhTkC04ENprOyXi7ZbUqiicF89uAGyt1oqgTUCD1VsLahqIcmrzgumNyTwLGWo17WDAa1/usDhetWAMhgzF/Cnf5ek0nK00m0YZGyc4LzgD0CROMASTWNg== ------END CERTIFICATE-----"; - - var samlresp = new Saml.Response(cert); - samlresp.LoadXml(@" - - http://idp.example.com/metadata.php - - - 99Bke1BpL1yOfGd5ADkGSle2sZg=OOyb3YtYQm3DC7gj6lQPM20r76HH4KvAE93f5xrIuIHGk8ZJlse4m8t4msLkhwUEAGwWOOVyHs8gChtN1m/P4pKCXyttO9Hev14Wz8E1R444kg5Yak+02FZ+Fn3VbbPq+kY4eYRkczNMphivWkdwc/QjDguNzGoKCEEtbBKDMGg= -MIICajCCAdOgAwIBAgIBADANBgkqhkiG9w0BAQ0FADBSMQswCQYDVQQGEwJ1czETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMT25lbG9naW4gSW5jMRcwFQYDVQQDDA5zcC5leGFtcGxlLmNvbTAeFw0xNDA3MTcxNDEyNTZaFw0xNTA3MTcxNDEyNTZaMFIxCzAJBgNVBAYTAnVzMRMwEQYDVQQIDApDYWxpZm9ybmlhMRUwEwYDVQQKDAxPbmVsb2dpbiBJbmMxFzAVBgNVBAMMDnNwLmV4YW1wbGUuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDZx+ON4IUoIWxgukTb1tOiX3bMYzYQiwWPUNMp+Fq82xoNogso2bykZG0yiJm5o8zv/sd6pGouayMgkx/2FSOdc36T0jGbCHuRSbtia0PEzNIRtmViMrt3AeoWBidRXmZsxCNLwgIV6dn2WpuE5Az0bHgpZnQxTKFek0BMKU/d8wIDAQABo1AwTjAdBgNVHQ4EFgQUGHxYqZYyX7cTxKVODVgZwSTdCnwwHwYDVR0jBBgwFoAUGHxYqZYyX7cTxKVODVgZwSTdCnwwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQ0FAAOBgQByFOl+hMFICbd3DJfnp2Rgd/dqttsZG/tyhILWvErbio/DEe98mXpowhTkC04ENprOyXi7ZbUqiicF89uAGyt1oqgTUCD1VsLahqIcmrzgumNyTwLGWo17WDAa1/usDhetWAMhgzF/Cnf5ek0nK00m0YZGyc4LzgD0CROMASTWNg== - - - - - http://idp.example.com/metadata.php - - _ce3d2948b4cf20146dee0a0b3dd6f69b6cf86f62d7 - - - - - - - http://sp.example.com/demo1/metadata.php - - - - - urn:oasis:names:tc:SAML:2.0:ac:classes:Password - - - - - test - - - test@example.com - - - users - examplerole1 - - - - -"); - Assert.IsTrue(samlresp.IsValid()); - - Assert.IsTrue(samlresp.GetNameID() == "_ce3d2948b4cf20146dee0a0b3dd6f69b6cf86f62d7"); - - Assert.IsTrue(samlresp.GetEmail() == "test@example.com"); - - Assert.IsTrue(samlresp.GetCustomAttribute("uid") == "test"); +using Saml; +using System.IO.Compression; +using System.Text; +using Shouldly; +using System.Security.Claims; + +namespace AspNetSaml.Tests +{ + [TestClass] + public class UnitTests + { + //cert and signature taken form here: www.samltool.com/generic_sso_res.php + + [TestMethod] + public void TestSamlResponseValidator() + { + var cert = @"-----BEGIN CERTIFICATE----- +MIICajCCAdOgAwIBAgIBADANBgkqhkiG9w0BAQ0FADBSMQswCQYDVQQGEwJ1czETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMT25lbG9naW4gSW5jMRcwFQYDVQQDDA5zcC5leGFtcGxlLmNvbTAeFw0xNDA3MTcxNDEyNTZaFw0xNTA3MTcxNDEyNTZaMFIxCzAJBgNVBAYTAnVzMRMwEQYDVQQIDApDYWxpZm9ybmlhMRUwEwYDVQQKDAxPbmVsb2dpbiBJbmMxFzAVBgNVBAMMDnNwLmV4YW1wbGUuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDZx+ON4IUoIWxgukTb1tOiX3bMYzYQiwWPUNMp+Fq82xoNogso2bykZG0yiJm5o8zv/sd6pGouayMgkx/2FSOdc36T0jGbCHuRSbtia0PEzNIRtmViMrt3AeoWBidRXmZsxCNLwgIV6dn2WpuE5Az0bHgpZnQxTKFek0BMKU/d8wIDAQABo1AwTjAdBgNVHQ4EFgQUGHxYqZYyX7cTxKVODVgZwSTdCnwwHwYDVR0jBBgwFoAUGHxYqZYyX7cTxKVODVgZwSTdCnwwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQ0FAAOBgQByFOl+hMFICbd3DJfnp2Rgd/dqttsZG/tyhILWvErbio/DEe98mXpowhTkC04ENprOyXi7ZbUqiicF89uAGyt1oqgTUCD1VsLahqIcmrzgumNyTwLGWo17WDAa1/usDhetWAMhgzF/Cnf5ek0nK00m0YZGyc4LzgD0CROMASTWNg== +-----END CERTIFICATE-----"; + + var samlresp = new Saml.Response(cert); + samlresp.LoadXml(@" + + http://idp.example.com/metadata.php + + + 99Bke1BpL1yOfGd5ADkGSle2sZg=OOyb3YtYQm3DC7gj6lQPM20r76HH4KvAE93f5xrIuIHGk8ZJlse4m8t4msLkhwUEAGwWOOVyHs8gChtN1m/P4pKCXyttO9Hev14Wz8E1R444kg5Yak+02FZ+Fn3VbbPq+kY4eYRkczNMphivWkdwc/QjDguNzGoKCEEtbBKDMGg= +MIICajCCAdOgAwIBAgIBADANBgkqhkiG9w0BAQ0FADBSMQswCQYDVQQGEwJ1czETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMT25lbG9naW4gSW5jMRcwFQYDVQQDDA5zcC5leGFtcGxlLmNvbTAeFw0xNDA3MTcxNDEyNTZaFw0xNTA3MTcxNDEyNTZaMFIxCzAJBgNVBAYTAnVzMRMwEQYDVQQIDApDYWxpZm9ybmlhMRUwEwYDVQQKDAxPbmVsb2dpbiBJbmMxFzAVBgNVBAMMDnNwLmV4YW1wbGUuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDZx+ON4IUoIWxgukTb1tOiX3bMYzYQiwWPUNMp+Fq82xoNogso2bykZG0yiJm5o8zv/sd6pGouayMgkx/2FSOdc36T0jGbCHuRSbtia0PEzNIRtmViMrt3AeoWBidRXmZsxCNLwgIV6dn2WpuE5Az0bHgpZnQxTKFek0BMKU/d8wIDAQABo1AwTjAdBgNVHQ4EFgQUGHxYqZYyX7cTxKVODVgZwSTdCnwwHwYDVR0jBBgwFoAUGHxYqZYyX7cTxKVODVgZwSTdCnwwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQ0FAAOBgQByFOl+hMFICbd3DJfnp2Rgd/dqttsZG/tyhILWvErbio/DEe98mXpowhTkC04ENprOyXi7ZbUqiicF89uAGyt1oqgTUCD1VsLahqIcmrzgumNyTwLGWo17WDAa1/usDhetWAMhgzF/Cnf5ek0nK00m0YZGyc4LzgD0CROMASTWNg== + + + + + http://idp.example.com/metadata.php + + _ce3d2948b4cf20146dee0a0b3dd6f69b6cf86f62d7 + + + + + + + http://sp.example.com/demo1/metadata.php + + + + + urn:oasis:names:tc:SAML:2.0:ac:classes:Password + + + + + test + + + test@example.com + + + users + examplerole1 + + + + +"); + + samlresp.IsValid().ShouldBeTrue(); + samlresp.GetNameID().ShouldBe("_ce3d2948b4cf20146dee0a0b3dd6f69b6cf86f62d7"); + samlresp.GetEmail().ShouldBe("test@example.com"); + samlresp.GetCustomAttribute("uid").ShouldBe("test"); } - [TestMethod] - public void TestSamlSignoutResponseValidator() - { - //this test's cert and signature borrowed from https://github.com/boxyhq/jackson/ + [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= + 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()); + 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.GetLogoutStatus() == "Success"); - } - [TestMethod] - public void TestSamlResponseValidatorAdvanced() - { + samlresp.IsValid().ShouldBeTrue(); + samlresp.GetLogoutStatus().ShouldBe("Success"); + } + + [TestMethod] + public void TestSamlResponseValidatorAdvanced() + { var cert = @"-----BEGIN CERTIFICATE----- MIIClTCCAX0CBgGICgolYzANBgkqhkiG9w0BAQsFADAOMQwwCgYDVQQDDANQT0MwHhcNMjMwNTExMDg1ODM3WhcNMzMwNTExMDkwMDE3WjAOMQwwCgYDVQQDDANQT0MwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCdKUug5y3ifMXH2kPGPib3APKzA1n9GEsAV304irs9oKK91iCpmQL0SfmMRtyWILPUTSSfKb+Ius2U9AgcjIs517DsbZYTZAglpuZ1DUZTN4IM2PRBrt2bpKv8vQTplesKw6QnWFGrjlOPtw1UmsTnciqiy71GHssSNlLvMObpyW02tt0mGbWQRvCeIwt+aXTB2xrK7buBNJ8yUwdJ0VOpfsUR0yLmV2N/oN0F+f1I/kxn/COEgFZiqJWWEyRCMCXafetU+dq8YMtcO149CKxK66WgTyanAjBf2jv7v5Gk3/0vrLFEIPtHBonDFFQeGw/sTV6bJG+tIS1CX5R/guZRAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAEdXFmQ0BNE4IrE+aEueIl/eyyb90jdU1gmtlrqIvR+RsuQlJzasjE5qW1vcZTdV+omQpeePnIY94KwkhbWwaMsshq7Zi7bbyNWmhc0Mo3o6ONbr3Q6fvfNBePbObGfVCFRT3mgwiqrR59Wmku4PopRS/DXYvbQoim5rxiClAHyN0PkcX6u5J7mmzV1RiZ5OE4fJkIHXXmvUc6NeeFOx8EUnEDrVbfyBn9AK0IZAoj7/jKAJPv5DsBZH3iuFwjSOCAIkpr3W0YcITBeRAvdAri9eFpJ3GO1ZKjynpQaUNWeB3JBjJeNBfQszzmEHlv3Lrayiv2+/uTjFZ2DT7jfxaMw= ------END CERTIFICATE-----"; - - var samlresp = new Saml.Response(cert); - samlresp.LoadXml(@"http://keycloak:1080/realms/POCUrJzr9Ja0f4Ks+K6TPEfQ53bw1veGXHtMZpLmRrr/ww=EAM65nY/e0YkK/H0nw+hdt6PhUIEs5jtftvP/NuHCSFjsVNj8L4jIT7Gvso8r9gSnwz0FJetVK16LjHdN+0f8Od2BDk9njD7KBQx9v9ich12zl1Ny+T6dLtc4XypkvoPwscna7KIQOEn8xeKBq4IbC+gPYfJEQ3GjnQ5JuXhJW5GValLELKWbH21oECRL6VAs7BAohQy2/BbTTGM1tbeuqWIZrqdP/KKOpiHxVIPwzwC8EuQmrhYiaJ9tOzNtBJGD5IW7L6Z6GIhVX2yQPuEW/gfb/bYCi6+0KD664YBICfyJLSarbcK6qgafP9YUdJ48qopiHXbuZ1m8ceCfC0Kow==MIIClTCCAX0CBgGICgolYzANBgkqhkiG9w0BAQsFADAOMQwwCgYDVQQDDANQT0MwHhcNMjMwNTExMDg1ODM3WhcNMzMwNTExMDkwMDE3WjAOMQwwCgYDVQQDDANQT0MwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCdKUug5y3ifMXH2kPGPib3APKzA1n9GEsAV304irs9oKK91iCpmQL0SfmMRtyWILPUTSSfKb+Ius2U9AgcjIs517DsbZYTZAglpuZ1DUZTN4IM2PRBrt2bpKv8vQTplesKw6QnWFGrjlOPtw1UmsTnciqiy71GHssSNlLvMObpyW02tt0mGbWQRvCeIwt+aXTB2xrK7buBNJ8yUwdJ0VOpfsUR0yLmV2N/oN0F+f1I/kxn/COEgFZiqJWWEyRCMCXafetU+dq8YMtcO149CKxK66WgTyanAjBf2jv7v5Gk3/0vrLFEIPtHBonDFFQeGw/sTV6bJG+tIS1CX5R/guZRAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAEdXFmQ0BNE4IrE+aEueIl/eyyb90jdU1gmtlrqIvR+RsuQlJzasjE5qW1vcZTdV+omQpeePnIY94KwkhbWwaMsshq7Zi7bbyNWmhc0Mo3o6ONbr3Q6fvfNBePbObGfVCFRT3mgwiqrR59Wmku4PopRS/DXYvbQoim5rxiClAHyN0PkcX6u5J7mmzV1RiZ5OE4fJkIHXXmvUc6NeeFOx8EUnEDrVbfyBn9AK0IZAoj7/jKAJPv5DsBZH3iuFwjSOCAIkpr3W0YcITBeRAvdAri9eFpJ3GO1ZKjynpQaUNWeB3JBjJeNBfQszzmEHlv3Lrayiv2+/uTjFZ2DT7jfxaMw=http://keycloak:1080/realms/POCguestWebApp3urn:oasis:names:tc:SAML:2.0:ac:classes:unspecifiedguest@guest.comGuestGuestuma_authorizationoffline_accessdefault-roles-pocview-profilemanage-accountmanage-account-linksSimpleUser"); - - Assert.IsTrue(samlresp.IsValid()); - - 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" })); - } - - [TestMethod] - public void TestSamlRequest() - { - var samlEndpoint = "http://saml-provider-that-we-use.com/login/"; - - var request = new AuthRequest( - "http://www.myapp.com", - "http://www.myapp.com/SamlConsume" - ); - - var r = request.GetRequest(); - - //decode the compressed base64 - var ms = new MemoryStream(Convert.FromBase64String(r)); - var ds = new DeflateStream(ms, CompressionMode.Decompress, true); - var output = new MemoryStream(); - ds.CopyTo(output); - - //get xml - var str = Encoding.UTF8.GetString(output.ToArray()); - - 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")); - - } - - [TestMethod] +-----END CERTIFICATE-----"; + + var samlresp = new Saml.Response(cert); + samlresp.LoadXml(@"http://keycloak:1080/realms/POCUrJzr9Ja0f4Ks+K6TPEfQ53bw1veGXHtMZpLmRrr/ww=EAM65nY/e0YkK/H0nw+hdt6PhUIEs5jtftvP/NuHCSFjsVNj8L4jIT7Gvso8r9gSnwz0FJetVK16LjHdN+0f8Od2BDk9njD7KBQx9v9ich12zl1Ny+T6dLtc4XypkvoPwscna7KIQOEn8xeKBq4IbC+gPYfJEQ3GjnQ5JuXhJW5GValLELKWbH21oECRL6VAs7BAohQy2/BbTTGM1tbeuqWIZrqdP/KKOpiHxVIPwzwC8EuQmrhYiaJ9tOzNtBJGD5IW7L6Z6GIhVX2yQPuEW/gfb/bYCi6+0KD664YBICfyJLSarbcK6qgafP9YUdJ48qopiHXbuZ1m8ceCfC0Kow==MIIClTCCAX0CBgGICgolYzANBgkqhkiG9w0BAQsFADAOMQwwCgYDVQQDDANQT0MwHhcNMjMwNTExMDg1ODM3WhcNMzMwNTExMDkwMDE3WjAOMQwwCgYDVQQDDANQT0MwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCdKUug5y3ifMXH2kPGPib3APKzA1n9GEsAV304irs9oKK91iCpmQL0SfmMRtyWILPUTSSfKb+Ius2U9AgcjIs517DsbZYTZAglpuZ1DUZTN4IM2PRBrt2bpKv8vQTplesKw6QnWFGrjlOPtw1UmsTnciqiy71GHssSNlLvMObpyW02tt0mGbWQRvCeIwt+aXTB2xrK7buBNJ8yUwdJ0VOpfsUR0yLmV2N/oN0F+f1I/kxn/COEgFZiqJWWEyRCMCXafetU+dq8YMtcO149CKxK66WgTyanAjBf2jv7v5Gk3/0vrLFEIPtHBonDFFQeGw/sTV6bJG+tIS1CX5R/guZRAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAEdXFmQ0BNE4IrE+aEueIl/eyyb90jdU1gmtlrqIvR+RsuQlJzasjE5qW1vcZTdV+omQpeePnIY94KwkhbWwaMsshq7Zi7bbyNWmhc0Mo3o6ONbr3Q6fvfNBePbObGfVCFRT3mgwiqrR59Wmku4PopRS/DXYvbQoim5rxiClAHyN0PkcX6u5J7mmzV1RiZ5OE4fJkIHXXmvUc6NeeFOx8EUnEDrVbfyBn9AK0IZAoj7/jKAJPv5DsBZH3iuFwjSOCAIkpr3W0YcITBeRAvdAri9eFpJ3GO1ZKjynpQaUNWeB3JBjJeNBfQszzmEHlv3Lrayiv2+/uTjFZ2DT7jfxaMw=http://keycloak:1080/realms/POCguestWebApp3urn:oasis:names:tc:SAML:2.0:ac:classes:unspecifiedguest@guest.comGuestGuestuma_authorizationoffline_accessdefault-roles-pocview-profilemanage-accountmanage-account-linksSimpleUser"); + + samlresp.IsValid().ShouldBeTrue(); + samlresp.GetCustomAttributeViaFriendlyName("givenName").ShouldBe("Guest"); + samlresp.GetCustomAttributeAsList("Role").ShouldBe(new List { "uma_authorization", "offline_access", "default-roles-poc", "view-profile", "manage-account", "manage-account-links", "SimpleUser" }, ignoreOrder: true); + } + + [TestMethod] + public void TestSamlRequest() + { + var request = new AuthRequest( + "http://www.myapp.com", + "http://www.myapp.com/SamlConsume" + ); + + var r = request.GetRequest(); + + //decode the compressed base64 + var ms = new MemoryStream(Convert.FromBase64String(r)); + var ds = new DeflateStream(ms, CompressionMode.Decompress, true); + var output = new MemoryStream(); + ds.CopyTo(output); + + //get xml + var str = Encoding.UTF8.GetString(output.ToArray()); + + str.ShouldEndWith(@"ProtocolBinding=""urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"" AssertionConsumerServiceURL=""http://www.myapp.com/SamlConsume"" xmlns:samlp=""urn:oasis:names:tc:SAML:2.0:protocol"">http://www.myapp.com"); + } + + [TestMethod] public void TestStringToByteArray() { //test that the old StringToByteArray was generating same result as the new Encoding.ASCII.GetBytes - var cert = @"-----BEGIN CERTIFICATE----- -MIICajCCAdOgAwIBAgIBADANBgkqhkiG9w0BAQ0FADBSMQswCQYDVQQGEwJ1czETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMT25lbG9naW4gSW5jMRcwFQYDVQQDDA5zcC5leGFtcGxlLmNvbTAeFw0xNDA3MTcxNDEyNTZaFw0xNTA3MTcxNDEyNTZaMFIxCzAJBgNVBAYTAnVzMRMwEQYDVQQIDApDYWxpZm9ybmlhMRUwEwYDVQQKDAxPbmVsb2dpbiBJbmMxFzAVBgNVBAMMDnNwLmV4YW1wbGUuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDZx+ON4IUoIWxgukTb1tOiX3bMYzYQiwWPUNMp+Fq82xoNogso2bykZG0yiJm5o8zv/sd6pGouayMgkx/2FSOdc36T0jGbCHuRSbtia0PEzNIRtmViMrt3AeoWBidRXmZsxCNLwgIV6dn2WpuE5Az0bHgpZnQxTKFek0BMKU/d8wIDAQABo1AwTjAdBgNVHQ4EFgQUGHxYqZYyX7cTxKVODVgZwSTdCnwwHwYDVR0jBBgwFoAUGHxYqZYyX7cTxKVODVgZwSTdCnwwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQ0FAAOBgQByFOl+hMFICbd3DJfnp2Rgd/dqttsZG/tyhILWvErbio/DEe98mXpowhTkC04ENprOyXi7ZbUqiicF89uAGyt1oqgTUCD1VsLahqIcmrzgumNyTwLGWo17WDAa1/usDhetWAMhgzF/Cnf5ek0nK00m0YZGyc4LzgD0CROMASTWNg== + var cert = @"-----BEGIN CERTIFICATE----- +MIICajCCAdOgAwIBAgIBADANBgkqhkiG9w0BAQ0FADBSMQswCQYDVQQGEwJ1czETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMT25lbG9naW4gSW5jMRcwFQYDVQQDDA5zcC5leGFtcGxlLmNvbTAeFw0xNDA3MTcxNDEyNTZaFw0xNTA3MTcxNDEyNTZaMFIxCzAJBgNVBAYTAnVzMRMwEQYDVQQIDApDYWxpZm9ybmlhMRUwEwYDVQQKDAxPbmVsb2dpbiBJbmMxFzAVBgNVBAMMDnNwLmV4YW1wbGUuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDZx+ON4IUoIWxgukTb1tOiX3bMYzYQiwWPUNMp+Fq82xoNogso2bykZG0yiJm5o8zv/sd6pGouayMgkx/2FSOdc36T0jGbCHuRSbtia0PEzNIRtmViMrt3AeoWBidRXmZsxCNLwgIV6dn2WpuE5Az0bHgpZnQxTKFek0BMKU/d8wIDAQABo1AwTjAdBgNVHQ4EFgQUGHxYqZYyX7cTxKVODVgZwSTdCnwwHwYDVR0jBBgwFoAUGHxYqZYyX7cTxKVODVgZwSTdCnwwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQ0FAAOBgQByFOl+hMFICbd3DJfnp2Rgd/dqttsZG/tyhILWvErbio/DEe98mXpowhTkC04ENprOyXi7ZbUqiicF89uAGyt1oqgTUCD1VsLahqIcmrzgumNyTwLGWo17WDAa1/usDhetWAMhgzF/Cnf5ek0nK00m0YZGyc4LzgD0CROMASTWNg== -----END CERTIFICATE-----"; - var x = StringToByteArray(cert); + var x = StringToByteArray(cert); var y = Encoding.ASCII.GetBytes(cert); - Assert.IsTrue(x.SequenceEqual(y)); - } - - private static byte[] StringToByteArray(string st) - { - byte[] bytes = new byte[st.Length]; - for (int i = 0; i < st.Length; i++) - { - bytes[i] = (byte)st[i]; - } - return bytes; - } - } + + x.SequenceEqual(y).ShouldBeTrue(); + } + + [TestMethod] + public void TestEncryptedAssertions() + { + // SAML values from https://www.samltool.com/generic_sso_res.php. + + var cert = Constants.Certificates.Certificate; + + var samlresp = new Saml.Response(cert); + + var xml = @$" + + http://idp.example.com/metadata.php + + + + + + + + + + + Pn5IVvMXk8cdvEJHQ0VGq9WMOaV2dg4QbuCdEt8Pc1yWZLUMlOghPK0pMevLsuKyBcUz/cIoQihsroBrQONrtLzhdqndGCtaZYoOdO2Lz0T5Huesqd6iEKihrtsLf4RGj2VX3XbtdQV5R/3IdnjGCgj4zClxtJb4P7gCApeQ/uIpjIuo/f1rwn9F0A+gbL5HOSicOrLMjTJVBwPR2EtwY1g7fomkKQtJpWiq2+LsXLoSwWIYM4wHyem6U+zX9qTr2yRefiNuyz1Ye0QCN1LXQCIYFrS0Mhao4MqXNXzkktmI1/FcAbGAwReUkAGY2UuS6+9MtPDuRFOk+8h+ldrxJBU= + + + + + WDObtBFd84WFugFF97T0SM3jd0QE6UPhVaiaLJsWRE9/rWN2oF7d0TfiYN9RmbcWYVMVdxl26o2QMX7nKv+ufesu+GSEMApKOKKjYqGYIWvSsnoeqZGoXftjl7+axLAt7XAqT4edh4IhaxM4k3aPdEFfc+fZVNzr9djUcOF7l7tFT29M0zeO/K/y6m9lvaWiRvdLf1K1Wqw8eramYvE7FhomwbIeWJguHznKrAfxhqw6HifIot/ox1pKpmyP49HLvq5tWQexTS+iNyktXzv0wZDOKjtfOy5xd5L8iXVBhY29a0tiFcnVrEWKZ7Z/kTKrl6uuxtiD6qOmlLQpcoSc1DeXnooBJn/PhIbsQZo6uKTtzMmRc62R3d32JZRUrg/Bpjtcb6nB4Iz4SSw4gSm4w7aNGKX3DqYpTAseEg082wtY4ZX8wTcb0pRV5Gc/h7vRNGtqD1q8/gmhQdpRZ468lg== + + + + + + + + + + + Pn5IVvMXk8cdvEJHQ0VGq9WMOaV2dg4QbuCdEt8Pc1yWZLUMlOghPK0pMevLsuKyBcUz/cIoQihsroBrQONrtLzhdqndGCtaZYoOdO2Lz0T5Huesqd6iEKihrtsLf4RGj2VX3XbtdQV5R/3IdnjGCgj4zClxtJb4P7gCApeQ/uIpjIuo/f1rwn9F0A+gbL5HOSicOrLMjTJVBwPR2EtwY1g7fomkKQtJpWiq2+LsXLoSwWIYM4wHyem6U+zX9qTr2yRefiNuyz1Ye0QCN1LXQCIYFrS0Mhao4MqXNXzkktmI1/FcAbGAwReUkAGY2UuS6+9MtPDuRFOk+8h+ldrxJBU= + + + + + WDObtBFd84WFugFF97T0SM3jd0QE6UPhVaiaLJsWRE9/rWN2oF7d0TfiYN9RmbcWYVMVdxl26o2QMX7nKv+ufesu+GSEMApKOKKjYqGYIWvSsnoeqZGoXftjl7+axLAt7XAqT4edh4IhaxM4k3aPdEFfc+fZVNzr9djUcOF7l7tFT29M0zeO/K/y6m9lvaWiRvdLf1K1Wqw8eramYvE7FhomwbIeWJguHznKrAfxhqw6HifIot/ox1pKpmyP49HLvq5tWQexTS+iNyktXzv0wZDOKjtfOy5xd5L8iXVBhY29a0tiFcnVrEWKZ7Z/kTKrl6uuxtiD6qOmlLQpcoSc1DeXnooBJn/PhIbsQZo6uKTtzMmRc62R3d32JZRUrg/Bpjtcb6nB4Iz4SSw4gSm4w7aNGKX3DqYpTAseEg082wtY4ZX8wTcb0pRV5Gc/h7vRNGtqD1q8/gmhQdpRZ468lg== + + + + "; + + samlresp.LoadXml(xml); + + var attributes = samlresp.GetEncryptedAttributes(); + + attributes.ShouldNotBeEmpty(); + + var expectedValues = new[] { + (ClaimTypes.MobilePhone, "555-555-1234"), + (ClaimTypes.MobilePhone, "555-555-4321"), + (ClaimTypes.MobilePhone, "555-555-1234"), + (ClaimTypes.MobilePhone, "555-555-4321") + }; + + attributes.ShouldBe(expectedValues); + + // The results can be filtered by claim type. + attributes.Where(x => x.Name == ClaimTypes.MobilePhone).ShouldBe(expectedValues); + attributes.Where(x => x.Name == ClaimTypes.Email).ShouldBeEmpty(); + } + + private static byte[] StringToByteArray(string st) + { + byte[] bytes = new byte[st.Length]; + for (int i = 0; i < st.Length; i++) + { + bytes[i] = (byte)st[i]; + } + return bytes; + } + } } \ No newline at end of file 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..f59ddd2 100644 --- a/AspNetSaml/Saml.cs +++ b/AspNetSaml/Saml.cs @@ -1,396 +1,479 @@ -/* Jitbit's simple SAML 2.0 component for ASP.NET - https://github.com/jitbit/AspNetSaml/ - (c) Jitbit LP, 2016-2023 - Use this freely under the Apache license (see https://choosealicense.com/licenses/apache-2.0/) -*/ - -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Xml; -using System.Security.Cryptography.X509Certificates; -using System.Security.Cryptography.Xml; -using System.IO.Compression; -using System.Text; - -namespace Saml -{ - public abstract class BaseResponse - { - protected XmlDocument _xmlDoc; - protected readonly X509Certificate2 _certificate; - protected XmlNamespaceManager _xmlNameSpaceManager; //we need this one to run our XPath queries on the SAML XML - - public string Xml { get { return _xmlDoc.OuterXml; } } - - public BaseResponse(string certificateStr, string responseString = null) : this(Encoding.ASCII.GetBytes(certificateStr), responseString) { } - - public BaseResponse(byte[] certificateBytes, string responseString = null) - { - _certificate = new X509Certificate2(certificateBytes); - if (responseString != null) - LoadXmlFromBase64(responseString); - } - - /// - /// Parse SAML response XML (in case was it not passed in constructor) - /// - public void LoadXml(string xml) - { - _xmlDoc = new XmlDocument { PreserveWhitespace = true, XmlResolver = null }; - _xmlDoc.LoadXml(xml); - - _xmlNameSpaceManager = GetNamespaceManager(); //lets construct a "manager" for XPath queries - } - - public void LoadXmlFromBase64(string response) - { - UTF8Encoding enc = new UTF8Encoding(); - LoadXml(enc.GetString(Convert.FromBase64String(response))); - } - - //an XML signature can "cover" not the whole document, but only a part of it - //.NET's built in "CheckSignature" does not cover this case, it will validate to true. - //We should check the signature reference, so it "references" the id of the root document element! If not - it's a hack - protected bool ValidateSignatureReference(SignedXml signedXml) - { - if (signedXml.SignedInfo.References.Count != 1) //no ref at all - return false; - - var reference = (Reference)signedXml.SignedInfo.References[0]; - var id = reference.Uri.Substring(1); - - var idElement = signedXml.GetIdElement(_xmlDoc, id); - - if (idElement == _xmlDoc.DocumentElement) - return true; - else //sometimes its not the "root" doc-element that is being signed, but the "assertion" element - { - var assertionNode = _xmlDoc.SelectSingleNode("/samlp:Response/saml:Assertion", _xmlNameSpaceManager) as XmlElement; - if (assertionNode != idElement) - return false; - } - - return true; - } - - //returns namespace manager, we need one b/c MS says so... Otherwise XPath doesnt work in an XML doc with namespaces - //see https://stackoverflow.com/questions/7178111/why-is-xmlnamespacemanager-necessary - private XmlNamespaceManager GetNamespaceManager() - { - XmlNamespaceManager manager = new XmlNamespaceManager(_xmlDoc.NameTable); - manager.AddNamespace("ds", SignedXml.XmlDsigNamespaceUrl); - manager.AddNamespace("saml", "urn:oasis:names:tc:SAML:2.0:assertion"); - manager.AddNamespace("samlp", "urn:oasis:names:tc:SAML:2.0:protocol"); - - return manager; - } - - /// - /// Checks the validity of SAML response (validate signature, check expiration date etc) - /// - /// - public bool IsValid() - { - XmlNodeList nodeList = _xmlDoc.SelectNodes("//ds:Signature", _xmlNameSpaceManager); - - SignedXml signedXml = new SignedXml(_xmlDoc); - - if (nodeList.Count == 0) return false; - - signedXml.LoadXml((XmlElement)nodeList[0]); - return ValidateSignatureReference(signedXml) && signedXml.CheckSignature(_certificate, true) && !IsExpired(); - } - - private bool IsExpired() - { - DateTime expirationDate = DateTime.MaxValue; - XmlNode node = _xmlDoc.SelectSingleNode("/samlp:Response/saml:Assertion[1]/saml:Subject/saml:SubjectConfirmation/saml:SubjectConfirmationData", _xmlNameSpaceManager); - if (node != null && node.Attributes["NotOnOrAfter"] != null) - { - DateTime.TryParse(node.Attributes["NotOnOrAfter"].Value, out expirationDate); - } - return DateTime.UtcNow > expirationDate.ToUniversalTime(); - } - } - - public class Response : BaseResponse - { - public Response(string certificateStr, string responseString = null) : base(certificateStr, responseString) { } - - public Response(byte[] certificateBytes, string responseString = null) : base(certificateBytes, responseString) { } - - /// - /// returns the User's login - /// - public string GetNameID() - { - XmlNode node = _xmlDoc.SelectSingleNode("/samlp:Response/saml:Assertion[1]/saml:Subject/saml:NameID", _xmlNameSpaceManager); - return node.InnerText; - } - - public virtual string GetUpn() - { - return GetCustomAttribute("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/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" - } - - 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("User.FirstName") - ?? GetCustomAttribute("givenName"); //some providers put last name into an attribute named "givenName" - } - - 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("User.LastName") - ?? GetCustomAttribute("sn"); //some providers put last name into an attribute named "sn" - } - - public virtual string GetDepartment() - { - return GetCustomAttribute("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/department") - ?? GetCustomAttribute("department"); - } - - public virtual string GetPhone() - { - return GetCustomAttribute("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/homephone") - ?? GetCustomAttribute("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/telephonenumber"); - } - - public virtual string GetCompany() - { - return GetCustomAttribute("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/companyname") - ?? GetCustomAttribute("organization") - ?? GetCustomAttribute("User.CompanyName"); - } - - public virtual string GetLocation() - { - return GetCustomAttribute("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/location") - ?? GetCustomAttribute("physicalDeliveryOfficeName"); - } - - public string GetCustomAttribute(string attr) - { - XmlNode node = _xmlDoc.SelectSingleNode("/samlp:Response/saml:Assertion[1]/saml:AttributeStatement/saml:Attribute[@Name='" + attr + "']/saml:AttributeValue", _xmlNameSpaceManager); - return node?.InnerText; - } - - public string GetCustomAttributeViaFriendlyName(string attr) - { - XmlNode node = _xmlDoc.SelectSingleNode("/samlp:Response/saml:Assertion[1]/saml:AttributeStatement/saml:Attribute[@FriendlyName='" + attr + "']/saml:AttributeValue", _xmlNameSpaceManager); - return node?.InnerText; - } - - public List GetCustomAttributeAsList(string attr) - { - XmlNodeList nodes = _xmlDoc.SelectNodes("/samlp:Response/saml:Assertion[1]/saml:AttributeStatement/saml:Attribute[@Name='" + attr + "']/saml:AttributeValue", _xmlNameSpaceManager); - return nodes?.Cast().Select(x => x.InnerText).ToList(); - } - } - - public class SignoutResponse : BaseResponse - { - public SignoutResponse(string certificateStr, string responseString = null) : base(certificateStr, responseString) { } - - public SignoutResponse(byte[] certificateBytes, string responseString = null) : base(certificateBytes, responseString) { } - - public string GetLogoutStatus() - { - XmlNode node = _xmlDoc.SelectSingleNode("/samlp:LogoutResponse/samlp:Status/samlp:StatusCode", _xmlNameSpaceManager); - return node?.Attributes["Value"].Value.Replace("urn:oasis:names:tc:SAML:2.0:status:", string.Empty); - } - } - - public abstract class BaseRequest - { - public string _id; - protected string _issue_instant; - - protected string _issuer; - - public BaseRequest(string issuer) - { - _id = "_" + Guid.NewGuid().ToString(); - _issue_instant = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ", System.Globalization.CultureInfo.InvariantCulture); - - _issuer = issuer; - } - - public abstract string GetRequest(); - - protected static string ConvertToBase64Deflated(string input) - { - //byte[] toEncodeAsBytes = System.Text.ASCIIEncoding.ASCII.GetBytes(input); - //return System.Convert.ToBase64String(toEncodeAsBytes); - - //https://stackoverflow.com/questions/25120025/acs75005-the-request-is-not-a-valid-saml2-protocol-message-is-showing-always%3C/a%3E - var memoryStream = new MemoryStream(); - using (var writer = new StreamWriter(new DeflateStream(memoryStream, CompressionMode.Compress, true), new UTF8Encoding(false))) - { - writer.Write(input); - writer.Close(); - } - string result = Convert.ToBase64String(memoryStream.GetBuffer(), 0, (int)memoryStream.Length, Base64FormattingOptions.None); - return result; - } - - /// - /// returns the URL you should redirect your users to (i.e. your SAML-provider login URL with the Base64-ed request in the querystring - /// - /// SAML provider login url - /// Optional state to pass through - /// - public string GetRedirectUrl(string samlEndpoint, string relayState = null) - { - var queryStringSeparator = samlEndpoint.Contains("?") ? "&" : "?"; - - var url = samlEndpoint + queryStringSeparator + "SAMLRequest=" + Uri.EscapeDataString(GetRequest()); - - if (!string.IsNullOrEmpty(relayState)) - { - url += "&RelayState=" + Uri.EscapeDataString(relayState); - } - - return url; - } - } - - public class AuthRequest : BaseRequest - { - private string _assertionConsumerServiceUrl; - - /// - /// Initializes new instance of AuthRequest - /// - /// put your EntityID here - /// put your return URL here - public AuthRequest(string issuer, string assertionConsumerServiceUrl) : base(issuer) - { - _assertionConsumerServiceUrl = assertionConsumerServiceUrl; - } - - /// - /// get or sets if ForceAuthn attribute is sent to IdP - /// - public bool ForceAuthn { get; set; } - - [Obsolete("Obsolete, will be removed")] - public enum AuthRequestFormat - { - Base64 = 1 - } - - [Obsolete("Obsolete, will be removed, use GetRequest()")] - public string GetRequest(AuthRequestFormat format) => GetRequest(); - - /// - /// returns SAML request as compressed and Base64 encoded XML. You don't need this method - /// - /// - public override string GetRequest() - { - using (StringWriter sw = new StringWriter()) - { - XmlWriterSettings xws = new XmlWriterSettings { OmitXmlDeclaration = true }; - - using (XmlWriter xw = XmlWriter.Create(sw, xws)) - { - xw.WriteStartElement("samlp", "AuthnRequest", "urn:oasis:names:tc:SAML:2.0:protocol"); - xw.WriteAttributeString("ID", _id); - xw.WriteAttributeString("Version", "2.0"); - xw.WriteAttributeString("IssueInstant", _issue_instant); - xw.WriteAttributeString("ProtocolBinding", "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"); - xw.WriteAttributeString("AssertionConsumerServiceURL", _assertionConsumerServiceUrl); - if (ForceAuthn) - xw.WriteAttributeString("ForceAuthn", "true"); - - xw.WriteStartElement("saml", "Issuer", "urn:oasis:names:tc:SAML:2.0:assertion"); - xw.WriteString(_issuer); - xw.WriteEndElement(); - - xw.WriteStartElement("samlp", "NameIDPolicy", "urn:oasis:names:tc:SAML:2.0:protocol"); - xw.WriteAttributeString("Format", "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"); - xw.WriteAttributeString("AllowCreate", "true"); - xw.WriteEndElement(); +/* Jitbit's simple SAML 2.0 component for ASP.NET + https://github.com/jitbit/AspNetSaml/ + (c) Jitbit LP, 2016-2023 + Use this freely under the Apache license (see https://choosealicense.com/licenses/apache-2.0/) +*/ + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Xml; +using System.Security.Cryptography.X509Certificates; +using System.Security.Cryptography.Xml; +using System.IO.Compression; +using System.Text; +using System.Security.Claims; +using System.Security.Cryptography; +using System.Xml.Linq; + +namespace Saml +{ + public abstract class BaseResponse + { + protected XmlDocument _xmlDoc; + protected readonly X509Certificate2 _certificate; + protected XmlNamespaceManager _xmlNameSpaceManager; //we need this one to run our XPath queries on the SAML XML + + public string Xml { get { return _xmlDoc.OuterXml; } } + + public BaseResponse(string certificateStr, string responseString = null) : this(Encoding.ASCII.GetBytes(certificateStr), responseString) { } + + public BaseResponse(byte[] certificateBytes, string responseString = null) : this(new X509Certificate2(certificateBytes), responseString) { } + + public BaseResponse(X509Certificate2 certificate, string responseString = null) + { + _certificate = certificate; + if (responseString != null) + LoadXmlFromBase64(responseString); + } + + /// + /// Parse SAML response XML (in case was it not passed in constructor) + /// + /// + /// Creates a default namespace manager if one is not provided. + public void LoadXml(string xml, XmlNamespaceManager namespaceManager = null) + { + _xmlDoc = new XmlDocument { PreserveWhitespace = true, XmlResolver = null }; + _xmlDoc.LoadXml(xml); + + _xmlNameSpaceManager = namespaceManager ?? GetNamespaceManager(); //lets construct a "manager" for XPath queries + } + + public void LoadXmlFromBase64(string response) + { + UTF8Encoding enc = new UTF8Encoding(); + LoadXml(enc.GetString(Convert.FromBase64String(response))); + } + + //an XML signature can "cover" not the whole document, but only a part of it + //.NET's built in "CheckSignature" does not cover this case, it will validate to true. + //We should check the signature reference, so it "references" the id of the root document element! If not - it's a hack + protected bool ValidateSignatureReference(SignedXml signedXml) + { + if (signedXml.SignedInfo.References.Count != 1) //no ref at all + return false; + + var reference = (Reference)signedXml.SignedInfo.References[0]; + var id = reference.Uri.Substring(1); + + var idElement = signedXml.GetIdElement(_xmlDoc, id); + + if (idElement == _xmlDoc.DocumentElement) + return true; + else //sometimes its not the "root" doc-element that is being signed, but the "assertion" element + { + var assertionNode = _xmlDoc.SelectSingleNode("/samlp:Response/saml:Assertion", _xmlNameSpaceManager) as XmlElement; + if (assertionNode != idElement) + return false; + } + + return true; + } + + //returns namespace manager, we need one b/c MS says so... Otherwise XPath doesnt work in an XML doc with namespaces + //see https://stackoverflow.com/questions/7178111/why-is-xmlnamespacemanager-necessary + private XmlNamespaceManager GetNamespaceManager() + { + var manager = new XmlNamespaceManager(_xmlDoc.NameTable); + + manager.AddNamespace("xs", "http://www.w3.org/2001/XMLSchema"); + manager.AddNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance"); + manager.AddNamespace("ds", SignedXml.XmlDsigNamespaceUrl); + manager.AddNamespace("ds", SignedXml.XmlDsigNamespaceUrl); + manager.AddNamespace("dsig", SignedXml.XmlDsigNamespaceUrl); + manager.AddNamespace("enc", EncryptedXml.XmlEncNamespaceUrl); + manager.AddNamespace("xenc", EncryptedXml.XmlEncNamespaceUrl); + manager.AddNamespace("xmlenc", EncryptedXml.XmlEncNamespaceUrl); + manager.AddNamespace("saml", "urn:oasis:names:tc:SAML:2.0:assertion"); + manager.AddNamespace("samlp", "urn:oasis:names:tc:SAML:2.0:protocol"); + + return manager; + } + + /// + /// Checks the validity of SAML response (validate signature, check expiration date etc) + /// + /// + public bool IsValid() + { + XmlNodeList nodeList = _xmlDoc.SelectNodes("//ds:Signature", _xmlNameSpaceManager); + + SignedXml signedXml = new SignedXml(_xmlDoc); + + if (nodeList.Count == 0) return false; + + signedXml.LoadXml((XmlElement)nodeList[0]); + return ValidateSignatureReference(signedXml) && + signedXml.CheckSignature(_certificate, true) && + !IsExpired(); + } + + private bool IsExpired() + { + DateTime expirationDate = DateTime.MaxValue; + XmlNode node = _xmlDoc.SelectSingleNode("/samlp:Response/saml:Assertion[1]/saml:Subject/saml:SubjectConfirmation/saml:SubjectConfirmationData", _xmlNameSpaceManager); + if (node != null && node.Attributes["NotOnOrAfter"] != null) + { + DateTime.TryParse(node.Attributes["NotOnOrAfter"].Value, out expirationDate); + } + return DateTime.UtcNow > expirationDate.ToUniversalTime(); + } + } + + public class Response : BaseResponse + { + public Response(string certificateStr, string responseString = null) : base(certificateStr, responseString) { } + + public Response(byte[] certificateBytes, string responseString = null) : base(certificateBytes, responseString) { } + + public Response(X509Certificate2 certificate, string responseString = null) : base(certificate, responseString) { } + + /// + /// returns the User's login + /// + public string GetNameID() + { + XmlNode node = _xmlDoc.SelectSingleNode("/samlp:Response/saml:Assertion[1]/saml:Subject/saml:NameID", _xmlNameSpaceManager); + return node.InnerText; + } + + public virtual string GetUpn() + { + return GetCustomAttribute(ClaimTypes.Upn); + } + + public virtual string GetEmail() + { + return GetCustomAttribute("User.email") + ?? GetCustomAttribute(ClaimTypes.Email) //some providers (for example Azure AD) put last name into an attribute named "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress" + ?? GetCustomAttribute("mail") //some providers put last name into an attribute named "mail" + ?? GetCustomAttribute("email"); //some providers put last name into an attribute named "email" + } + + public virtual string GetFirstName() + { + return GetCustomAttribute("first_name") + ?? GetCustomAttribute(ClaimTypes.GivenName) //some providers (for example Azure AD) put last name into an attribute named "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname" + ?? GetCustomAttribute("User.FirstName") + ?? GetCustomAttribute("givenName"); //some providers put last name into an attribute named "givenName" + } + + public virtual string GetLastName() + { + return GetCustomAttribute("last_name") + ?? GetCustomAttribute(ClaimTypes.Surname) //some providers (for example Azure AD) put last name into an attribute named "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname" + ?? GetCustomAttribute("User.LastName") + ?? GetCustomAttribute("sn"); //some providers put last name into an attribute named "sn" + } + + public virtual string GetDepartment() + { + return GetCustomAttribute("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/department") + ?? GetCustomAttribute("department"); + } + + public virtual string GetPhone() + { + return GetCustomAttribute(ClaimTypes.HomePhone) + ?? GetCustomAttribute(ClaimTypes.MobilePhone) + ?? GetCustomAttribute(ClaimTypes.OtherPhone) + ?? GetCustomAttribute("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/telephonenumber"); + } + + public virtual string GetCompany() + { + return GetCustomAttribute("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/companyname") + ?? GetCustomAttribute("organization") + ?? GetCustomAttribute("User.CompanyName"); + } + + public virtual string GetLocation() + { + return GetCustomAttribute("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/location") + ?? GetCustomAttribute("physicalDeliveryOfficeName"); + } + + public string GetCustomAttribute(string attr) + { + XmlNode node = _xmlDoc.SelectSingleNode("/samlp:Response/saml:Assertion[1]/saml:AttributeStatement/saml:Attribute[@Name='" + attr + "']/saml:AttributeValue", _xmlNameSpaceManager); + return node?.InnerText; + } + + public string GetCustomAttributeViaFriendlyName(string attr) + { + XmlNode node = _xmlDoc.SelectSingleNode("/samlp:Response/saml:Assertion[1]/saml:AttributeStatement/saml:Attribute[@FriendlyName='" + attr + "']/saml:AttributeValue", _xmlNameSpaceManager); + return node?.InnerText; + } + + public List GetCustomAttributeAsList(string attr) + { + XmlNodeList nodes = _xmlDoc.SelectNodes("/samlp:Response/saml:Assertion[1]/saml:AttributeStatement/saml:Attribute[@Name='" + attr + "']/saml:AttributeValue", _xmlNameSpaceManager); + return nodes?.Cast().Select(x => x.InnerText).ToList(); + } + + /// + /// Decrypts and returns any encrypted attributes using the SAML Service Provider's certificate private key. + /// + /// + /// A list of SAML attribute Name/Value tuples. + /// + /// Adapted from: https://github.com/ruialexrib/Programatica.Auth.SAML.ServiceProviderUtils/blob/master/src/Utils/AssertionParserUtils.cs. + /// + public IEnumerable<(string Name, string Value)> GetEncryptedAttributes() + { + if (_certificate?.HasPrivateKey != true) + { + yield break; + } + + var dataElements = _xmlDoc.SelectNodes("/samlp:Response/saml:EncryptedAssertion/xenc:EncryptedData", _xmlNameSpaceManager); + + if (dataElements == null || dataElements.Count == 0) + { + yield break; + } + + var parserContext = new XmlParserContext(null, _xmlNameSpaceManager, null, XmlSpace.None); - /*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(); + foreach (XmlNode element in dataElements) + { + var encryptionAlgorithm = element.SelectSingleNode("//xenc:EncryptionMethod", _xmlNameSpaceManager).Attributes["Algorithm"]?.Value; + var encryptionKeyAlgorithm = element.SelectSingleNode("//ds:KeyInfo/xenc:EncryptedKey/xenc:EncryptionMethod", _xmlNameSpaceManager)?.Attributes["Algorithm"]?.Value; + var encryptionKeyCipherValue = element.SelectSingleNode("//ds:KeyInfo/xenc:EncryptedKey/xenc:CipherData/xenc:CipherValue", _xmlNameSpaceManager)?.InnerText; + + using var key = Rijndael.Create(encryptionAlgorithm); + key.Key = EncryptedXml.DecryptKey( + Convert.FromBase64String(encryptionKeyCipherValue), + _certificate.GetRSAPrivateKey(), + useOAEP: encryptionKeyAlgorithm == EncryptedXml.XmlEncRSAOAEPUrl + ); + + var encryptedXml = new EncryptedXml(); + var encryptedData = new EncryptedData(); + encryptedData.LoadXml((XmlElement)element); + + using var reader = new XmlTextReader( + Encoding.UTF8.GetString( + encryptedXml.DecryptData(encryptedData, key) + ), + XmlNodeType.Element, + parserContext); + + var attributeElement = XElement.Load(reader); + + // Attribute claim type. + var attributeType = attributeElement.Attribute("Name")?.Value; + + // Attribute values. + foreach (var value in attributeElement.Descendants().Where(e => e?.Name?.LocalName == "AttributeValue")) + { + yield return (Name: attributeType, Value: value.Value); + } + } + } + } + + public class SignoutResponse : BaseResponse + { + public SignoutResponse(string certificateStr, string responseString = null) : base(certificateStr, responseString) { } + + public SignoutResponse(byte[] certificateBytes, string responseString = null) : base(certificateBytes, responseString) { } + + public string GetLogoutStatus() + { + XmlNode node = _xmlDoc.SelectSingleNode("/samlp:LogoutResponse/samlp:Status/samlp:StatusCode", _xmlNameSpaceManager); + return node?.Attributes["Value"].Value.Replace("urn:oasis:names:tc:SAML:2.0:status:", string.Empty); + } + } + + public abstract class BaseRequest + { + public string _id; + protected string _issue_instant; + + protected string _issuer; + + public BaseRequest(string issuer) + { + _id = "_" + Guid.NewGuid().ToString(); + _issue_instant = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ", System.Globalization.CultureInfo.InvariantCulture); + + _issuer = issuer; + } + + public abstract string GetRequest(); + + protected static string ConvertToBase64Deflated(string input) + { + //byte[] toEncodeAsBytes = System.Text.ASCIIEncoding.ASCII.GetBytes(input); + //return System.Convert.ToBase64String(toEncodeAsBytes); + + //https://stackoverflow.com/questions/25120025/acs75005-the-request-is-not-a-valid-saml2-protocol-message-is-showing-always%3C/a%3E + var memoryStream = new MemoryStream(); + using (var writer = new StreamWriter(new DeflateStream(memoryStream, CompressionMode.Compress, true), new UTF8Encoding(false))) + { + writer.Write(input); + writer.Close(); + } + string result = Convert.ToBase64String(memoryStream.GetBuffer(), 0, (int)memoryStream.Length, Base64FormattingOptions.None); + return result; + } + + /// + /// returns the URL you should redirect your users to (i.e. your SAML-provider login URL with the Base64-ed request in the querystring + /// + /// SAML provider login url + /// Optional state to pass through + /// + public string GetRedirectUrl(string samlEndpoint, string relayState = null) + { + var queryStringSeparator = samlEndpoint.Contains("?") ? "&" : "?"; + + var url = samlEndpoint + queryStringSeparator + "SAMLRequest=" + Uri.EscapeDataString(GetRequest()); + + if (!string.IsNullOrEmpty(relayState)) + { + url += "&RelayState=" + Uri.EscapeDataString(relayState); + } + + return url; + } + } + + public class AuthRequest : BaseRequest + { + private string _assertionConsumerServiceUrl; + + /// + /// Initializes new instance of AuthRequest + /// + /// put your EntityID here + /// put your return URL here + public AuthRequest(string issuer, string assertionConsumerServiceUrl) : base(issuer) + { + _assertionConsumerServiceUrl = assertionConsumerServiceUrl; + } + + /// + /// get or sets if ForceAuthn attribute is sent to IdP + /// + public bool ForceAuthn { get; set; } + + [Obsolete("Obsolete, will be removed")] + public enum AuthRequestFormat + { + Base64 = 1 + } + + [Obsolete("Obsolete, will be removed, use GetRequest()")] + public string GetRequest(AuthRequestFormat format) => GetRequest(); + + /// + /// returns SAML request as compressed and Base64 encoded XML. You don't need this method + /// + /// + public override string GetRequest() + { + using (StringWriter sw = new StringWriter()) + { + XmlWriterSettings xws = new XmlWriterSettings { OmitXmlDeclaration = true }; + + using (XmlWriter xw = XmlWriter.Create(sw, xws)) + { + xw.WriteStartElement("samlp", "AuthnRequest", "urn:oasis:names:tc:SAML:2.0:protocol"); + xw.WriteAttributeString("ID", _id); + xw.WriteAttributeString("Version", "2.0"); + xw.WriteAttributeString("IssueInstant", _issue_instant); + xw.WriteAttributeString("ProtocolBinding", "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"); + xw.WriteAttributeString("AssertionConsumerServiceURL", _assertionConsumerServiceUrl); + if (ForceAuthn) + xw.WriteAttributeString("ForceAuthn", "true"); + + xw.WriteStartElement("saml", "Issuer", "urn:oasis:names:tc:SAML:2.0:assertion"); + xw.WriteString(_issuer); + xw.WriteEndElement(); + + xw.WriteStartElement("samlp", "NameIDPolicy", "urn:oasis:names:tc:SAML:2.0:protocol"); + xw.WriteAttributeString("Format", "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"); + xw.WriteAttributeString("AllowCreate", "true"); + xw.WriteEndElement(); + + /*xw.WriteStartElement("samlp", "RequestedAuthnContext", "urn:oasis:names:tc:SAML:2.0:protocol"); + xw.WriteAttributeString("Comparison", "exact"); + xw.WriteStartElement("saml", "AuthnContextClassRef", "urn:oasis:names:tc:SAML:2.0:assertion"); + xw.WriteString("urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport"); + xw.WriteEndElement(); xw.WriteEndElement();*/ - xw.WriteEndElement(); - } - - return ConvertToBase64Deflated(sw.ToString()); - } - } - } - - public class SignoutRequest : BaseRequest - { - private string _nameId; - - public SignoutRequest(string issuer, string nameId) : base(issuer) - { - _nameId = nameId; - } - - public override string GetRequest() - { - using (StringWriter sw = new StringWriter()) - { - XmlWriterSettings xws = new XmlWriterSettings { OmitXmlDeclaration = true }; - - using (XmlWriter xw = XmlWriter.Create(sw, xws)) - { - xw.WriteStartElement("samlp", "LogoutRequest", "urn:oasis:names:tc:SAML:2.0:protocol"); - xw.WriteAttributeString("ID", _id); - xw.WriteAttributeString("Version", "2.0"); - xw.WriteAttributeString("IssueInstant", _issue_instant); - - xw.WriteStartElement("saml", "Issuer", "urn:oasis:names:tc:SAML:2.0:assertion"); - xw.WriteString(_issuer); - xw.WriteEndElement(); - - xw.WriteStartElement("saml", "NameID", "urn:oasis:names:tc:SAML:2.0:assertion"); - xw.WriteString(_nameId); - xw.WriteEndElement(); - - xw.WriteEndElement(); - } - - return ConvertToBase64Deflated(sw.ToString()); - } - } - } - - public static class MetaData - { - /// - /// generates XML string describing service provider metadata based on provided EntiytID and Consumer URL - /// - /// - /// - /// - public static string Generate(string entityId, string assertionConsumerServiceUrl) - { - return $@" + xw.WriteEndElement(); + } + + return ConvertToBase64Deflated(sw.ToString()); + } + } + } + + public class SignoutRequest : BaseRequest + { + private string _nameId; + + public SignoutRequest(string issuer, string nameId) : base(issuer) + { + _nameId = nameId; + } + + public override string GetRequest() + { + using (StringWriter sw = new StringWriter()) + { + XmlWriterSettings xws = new XmlWriterSettings { OmitXmlDeclaration = true }; + + using (XmlWriter xw = XmlWriter.Create(sw, xws)) + { + xw.WriteStartElement("samlp", "LogoutRequest", "urn:oasis:names:tc:SAML:2.0:protocol"); + xw.WriteAttributeString("ID", _id); + xw.WriteAttributeString("Version", "2.0"); + xw.WriteAttributeString("IssueInstant", _issue_instant); + + xw.WriteStartElement("saml", "Issuer", "urn:oasis:names:tc:SAML:2.0:assertion"); + xw.WriteString(_issuer); + xw.WriteEndElement(); + + xw.WriteStartElement("saml", "NameID", "urn:oasis:names:tc:SAML:2.0:assertion"); + xw.WriteString(_nameId); + xw.WriteEndElement(); + + xw.WriteEndElement(); + } + + return ConvertToBase64Deflated(sw.ToString()); + } + } + } + + public static class MetaData + { + /// + /// generates XML string describing service provider metadata based on provided EntiytID and Consumer URL + /// + /// + /// + /// + public static string Generate(string entityId, string assertionConsumerServiceUrl) + { + return $@" @@ -404,6 +487,6 @@ public static string Generate(string entityId, string assertionConsumerServiceUr index=""1"" /> "; - } - } + } + } } \ No newline at end of file