|
| 1 | +using System; |
| 2 | +using System.Collections.Generic; |
| 3 | +using System.IO; |
| 4 | +using System.Linq; |
| 5 | +using System.Reflection; |
| 6 | +using System.Security.Cryptography; |
| 7 | +using System.Security.Policy; |
| 8 | +using System.Text; |
| 9 | +using System.Text.Encodings.Web; |
| 10 | +using System.Threading.Tasks; |
| 11 | +using System.Web; |
| 12 | +using System.Xml; |
| 13 | +using GamSaml20; |
| 14 | +using GamSaml20.Utils; |
| 15 | +using NUnit.Framework; |
| 16 | + |
| 17 | +namespace GamTest.Saml20 |
| 18 | +{ |
| 19 | + [TestFixture] |
| 20 | + public class TestRedirectBinding |
| 21 | + { |
| 22 | + private static string resources; |
| 23 | + private static string BASE_PATH; |
| 24 | + |
| 25 | + private static string password; |
| 26 | + private static RedirectBinding redirectBindingLogoutResponse; |
| 27 | + |
| 28 | + [SetUp] |
| 29 | + public virtual void SetUp() |
| 30 | + { |
| 31 | + BASE_PATH = GetStartupDirectory(); |
| 32 | + resources = Path.Combine(BASE_PATH, "Resources", "dummycerts"); |
| 33 | + password = "dummy1"; |
| 34 | + |
| 35 | + redirectBindingLogoutResponse = new RedirectBinding(); |
| 36 | + string logoutResponse = "SAMLResponse=fVHLauQwEPwVo7tsSX7JwnYICVkCyR52ZnPYyyBLPYnBozZueZPPX8%2BEOQSWHJvqquqqbm8%2BTlPyFxYaMXRMpoIlEBz6Mbx27Pf%2BgWuWULTB2wkDdCwgu%2BlbsqdpNk%2F4imv8BTRjIEg2pUDmAnVsXYJBSyOZYE9AJjqzu31%2BMioVZl4wosOJJfdAcQw2XszfYpzJZNmEzk5vSDH7cft8OLMOdxgCuIjLT4gPmaUJU0vzB0se7zt2GKq8sMo7nov6yItKAdd60FwcRV0rXRWFhm01XC%2Fd40aq68ZrbS23FQheQJVz7SvNS%2B%2B9ODYbLO1GIlrhMZwbiB1TQpVclFw2e1kaKY3KU1XWf1jycm1wy8f69kJbPhv5vgtLBMs5P%2Buv%2BSlS%2Bj4Gj%2B%2BUBohZCa4ehmPDpa7VFs81fFBCcitBatk01ZCXWZt9el5%2Fs4s2rvR1ukMPyYudVvj%2BJrpsm93qHBCxrG%2Bzr6LZ%2F%2F7f%2FwM%3D&RelayState=http%3A%2F%2Frelaystate.com&SigAlg=http%3A%2F%2Fwww.w3.org%2F2001%2F04%2Fxmldsig-more%23rsa-sha256&Signature=ZhxqgSDAmtwxtUAXCafCNAXKLwL9iPgsqInuZfQ97dyPsGyszpgJftjgHBtoQpz159NjFpX0dGicVier2TQa82JBqgxUvdPT6mg%2FppdG7Z%2BnOXNttflqCd7mA3b%2FUOmWE4XgODz2mym%2BNPBmETAYmKofXo5ghpQc8IgGpI166%2F5VOwwhLcrg76HeYSxubxS4BoFUtLmpRnkaww9VQPZPIyh4kBmsCqe%2FV4QvM626ehdXDjPIciBgylt2ENMfQGZo83ubMB7KxgDNdErBgmTpILxftLn3ZH0FJAbM%2B3bzj6DFJ1yLuyUnUbdxOjoKaRskil853jKqmbvQtxRQ4QvZIg%3D%3D"; |
| 37 | + redirectBindingLogoutResponse.Init(logoutResponse); |
| 38 | + } |
| 39 | + |
| 40 | + [Test] |
| 41 | + public void TestSignatureValidation_true() |
| 42 | + { |
| 43 | + SamlParms parms = new SamlParms(); |
| 44 | + parms.TrustedCertPath = Path.Combine(resources, "saml20", "javacert.crt"); |
| 45 | + Console.WriteLine(parms.TrustedCertPath); |
| 46 | + Assert.IsTrue(redirectBindingLogoutResponse.VerifySignatures(parms), "TestSignatureValidation_true Logout"); |
| 47 | + |
| 48 | + } |
| 49 | + |
| 50 | + [Test] |
| 51 | + public void TestSignatureValidation_false() |
| 52 | + { |
| 53 | + SamlParms parms = new SamlParms(); |
| 54 | + parms.TrustedCertPath = Path.Combine(resources, "saml20", "sha512_cert.crt"); |
| 55 | + Assert.IsFalse(redirectBindingLogoutResponse.VerifySignatures(parms), "TestSignatureValidation_false Logout"); |
| 56 | + } |
| 57 | + |
| 58 | + [Test] |
| 59 | + public void TestGetLogoutAssertions() |
| 60 | + { |
| 61 | + string expected = "{\"Destination\": \"https://localhost/GAM_SAML_ConnectorNetF/aslo.aspx\",\"InResponseTo\": \"_779d88aa-a6e0-4e63-8d68-5ddd0f97791a\",\"Value\": \"urn:oasis:names:tc:SAML:2.0:status:Success\",\"Issuer\": \"https://sts.windows.net/5ec7bbf9-1872-46c9-b201-a1e181996b35/\" }"; |
| 62 | + Assert.AreEqual(expected, redirectBindingLogoutResponse.GetLogoutAssertions()); |
| 63 | + } |
| 64 | + |
| 65 | + [Test] |
| 66 | + public void TestIsLogout() |
| 67 | + { |
| 68 | + string loginResponse = "SAMLResponse=5Vbfb%2BJGEH7uSf0frH03%2FoGNjRW40qSpkJJcFOipvZdoWQ%2FgO3vX3V0Hcn99Zw0mYHqE3lWqTn2yPDOe%2Beb7dmZ98XZd5NYTSJUJPiBexyUWcCbSjC8G5LfptR0TS2nKU5oLDgPCBXk7vFC0yP0yeQBVCq7AwiRcJRvrgFSSJ4KqTCWcFqASzZLJ6PYm8TtuUkqhBRM52X6zVgOy1LpMHGe1WnVW3Y6QC8d3Xc%2F5%2FfZmwpZQUGJdgdIZp7oGuQ3PBaP5Uijt%2FDq6fTQFHi8F58C0kHegrx1KmepQVa6JNb4akCy1w27kRj0v7rluHHleEEZe0I17YRB3%2BxjEm36mYkAeGcy7bBa5duy5gR0wd2bHaRrazAv6EaPhPEgR2VipCsbcUKQHxHf90HZD23enXj8JeokXdmK394FY7xuKkQQy%2FPHNDxsOk%2Fp7uU%2Fgaf6oUiAND8S6FrKg%2BnS4sWDj8zo0Aa4z%2FUyGe4SLT5p2mCgcWH%2Fyn0q6iP7sxn88FM%2FTMI0unH2QL6jLZKKprpSxtEyXIgXrPc0rOA1M1dHJpGIMlCJOndw5yr5ladR0%2FVVEbdWPfb%2FXDyK%2F6wZ9PAhhL%2FK9KHY9P%2Br77jco2ZLyv5ClgTCpZh9xAGpTY7vDWuOrV2F5Ha8Nq6BZPkpTaQQa0hw%2B4hqQovMZZjTPhfppYQIMygbQptRB9S0inMx5ZrIaDW9BL0V6miFWJDOgEuSG4lP5rqimXze8d0K%2F4%2B%2FkaK5BtiUPvY3kEUr%2BACwrMzDn4p8vH2fLh%2FPFBjYKOscSbnvG2DQzgcog%2FhlQHzg6oXtwz2zrQKdRlWKDDJBFLTO2g3UUMJzcY8sN2p31oMcvJWv8Lw3ttzmq9JKb0YcCqbbq1zPmcYLHE1ONeQrrc5VvtY6FEJKGtW73%2FOK5zHGjPMB8eHLpsISZODTf42MlZHqPFx7qCelUUo4nVOoXlv4me4vHFrIDx46qAxI1Mj6rNBx6j9yWmVUcQVySv5gxJrXhnN1FMYWxNmtihnGsNaW7MvVNsLvqs9fvejurFWeAPwgqS%2FRziSjXuPExIV%2Bct4YOq7cobXyv0nKdSaXN6%2FdCzWKdwtO%2Fy8GNWGT8%2F3k0jqwHA7XzNn8ZaNz9vjQ30fAv&RelayState=http%3A%2F%2Frelaystate.com&SigAlg=http%3A%2F%2Fwww.w3.org%2F2001%2F04%2Fxmldsig-more%23rsa-sha256&Signature=aeNkIQLkkrNIxVgd1slzZXJpkEzvU0LIwqMR9wWLRT%2FjMHo7ldaCeGlFk3H%2Bbr4l3qEttjsTBWgTGgPDgzax7DDCUSvJPdAh0YB14T9oZ243cxap2OOi483TkBPt%2BwM6Q4AaePWbH1NdUvFUmP9ovl4Ub3iC4O%2FmZFRR3l4TU4z5ZR5OO8%2FFm%2BppvYXf%2FJDbsTLkKgF72a1lD1YhNWdqYKx3%2BQ22x94osmXis3omG7cdNDlo8ULesWL2RVXzftjmHa9zqWidTrHjyA6fSouTV3pQHmzrI8t9g3tuk5jKzTbOPmF2KBhEPzvN26jH2Bdy5b4PCvkJ1L9VeJKlGwBejQ%3D%3D"; |
| 69 | + RedirectBinding redirectBindingLoginResponse = new RedirectBinding(); |
| 70 | + redirectBindingLoginResponse.Init(loginResponse); |
| 71 | + Assert.IsTrue(redirectBindingLogoutResponse.IsLogout(), "TestIsLogout Logout"); |
| 72 | + Assert.IsFalse(redirectBindingLoginResponse.IsLogout(), "TestIsLogout Login"); |
| 73 | + } |
| 74 | + |
| 75 | + [Test] |
| 76 | + public void TestGetRelayState() |
| 77 | + { |
| 78 | + string expected = "http://relaystate.com"; |
| 79 | + Assert.AreEqual(expected, redirectBindingLogoutResponse.GetRelayState()); |
| 80 | + } |
| 81 | + |
| 82 | + |
| 83 | + [Test] |
| 84 | + public void TestLoginRequest() |
| 85 | + { |
| 86 | + string function = "Login"; |
| 87 | + RedirectBinding redirectBinding = new RedirectBinding(); |
| 88 | + SamlParms parms = CreateParameters(); |
| 89 | + string samlRequest = RedirectBinding.Login(parms, "http://relaystate.com"); |
| 90 | + string queryString = GetQueryString(samlRequest); |
| 91 | + Dictionary<string, string> redirectMessage = ParseRedirect(queryString); |
| 92 | + |
| 93 | + //test login request parameters |
| 94 | + TestRequestParameters(redirectMessage, function); |
| 95 | + |
| 96 | + //test login signature |
| 97 | + TestRequestSignature(redirectMessage, function); |
| 98 | + |
| 99 | + //test login request xml parameters |
| 100 | + string xml = System.Text.Encoding.UTF8.GetString(GamSaml20.Utils.Encoding.DecodeAndInflateXmlParameter(redirectMessage["SAMLRequest"])); |
| 101 | + |
| 102 | + string expectedXml = $"<AuthnRequest ID=\"_idtralala\" Version=\"2.0\" IssueInstant=\"{GetIssuerInstant(xml, "AuthnRequest").Trim()}\" Destination=\"http://endpoint/saml\" AssertionConsumerServiceURL=\"http://myapp.com/acs\" ForceAuthn=\"false\" xmlns=\"urn:oasis:names:tc:SAML:2.0:protocol\">\r\n" + |
| 103 | + " <Issuer xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\">EntityID</Issuer>\r\n" + |
| 104 | + " <NameIDPolicy Format=\"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress\" AllowCreate=\"true\" SPNameQualifier=\"SPEntityID\" />\r\n" + |
| 105 | + " <RequestedAuthnContext Comparison=\"exact\">\r\n" + |
| 106 | + " <AuthnContextClassRef xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\">urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</AuthnContextClassRef>\r\n" + |
| 107 | + " </RequestedAuthnContext>\r\n" + |
| 108 | + "</AuthnRequest>"; |
| 109 | + |
| 110 | + //"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?><saml2p:AuthnRequest AssertionConsumerServiceURL=\"http://myapp.com/acs\" Destination=\"http://endpoint/saml\" ForceAuthn=\"false\" ID=\"_idtralala\" IssueInstant=\"" + GetIssuerInstant(xml, "saml2p:AuthnRequest") + "\" Version=\"2.0\" xmlns:saml2p=\"urn:oasis:names:tc:SAML:2.0:protocol\"><saml2:Issuer xmlns:saml2=\"urn:oasis:names:tc:SAML:2.0:assertion\"/><saml2p:NameIDPolicy AllowCreate=\"true\" Format=\"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress\" SPNameQualifier=\"SPEntityID\"/><saml2p:RequestedAuthnContext Comparison=\"exact\"><saml2:AuthnContextClassRef xmlns:saml2=\"urn:oasis:names:tc:SAML:2.0:assertion\">urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</saml2:AuthnContextClassRef></saml2p:RequestedAuthnContext></saml2p:AuthnRequest>"; |
| 111 | + Assert.AreEqual(expectedXml, xml, "Test Login request xml parameters"); |
| 112 | + |
| 113 | + } |
| 114 | + |
| 115 | + [Test] |
| 116 | + public void TestLogoutRequest() |
| 117 | + { |
| 118 | + string function = "Logout"; |
| 119 | + RedirectBinding redirectBinding = new RedirectBinding(); |
| 120 | + SamlParms parms = CreateParameters(); |
| 121 | + string samlRequest = RedirectBinding.Logout(parms, "http://relaystate.com"); |
| 122 | + string queryString = GetQueryString(samlRequest); |
| 123 | + Dictionary<string, string> redirectMessage = ParseRedirect(queryString); |
| 124 | + |
| 125 | + //test logout request parameters |
| 126 | + TestRequestParameters(redirectMessage, function); |
| 127 | + |
| 128 | + //test logout signature |
| 129 | + TestRequestSignature(redirectMessage, function); |
| 130 | + |
| 131 | + //test logout request xml parameters |
| 132 | + string xml = System.Text.Encoding.UTF8.GetString(GamSaml20.Utils.Encoding.DecodeAndInflateXmlParameter(redirectMessage["SAMLRequest"])); |
| 133 | + string expectedXml = $"<LogoutRequest ID=\"_idtralala\" Version=\"2.0\" IssueInstant=\"{GetIssuerInstant(xml, "LogoutRequest")}\" Destination=\"http://idp.com/slo\" Reason=\"urn:oasis:names:tc:SAML:2.0:logout:user\" xmlns=\"urn:oasis:names:tc:SAML:2.0:protocol\">\r\n" + |
| 134 | + " <Issuer xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\">SPEntityID</Issuer>\r\n" + |
| 135 | + " <NameID xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\">nameID</NameID>\r\n" + |
| 136 | + " <SessionIndex>123456789</SessionIndex>\r\n" + |
| 137 | + "</LogoutRequest>"; |
| 138 | + Assert.AreEqual(expectedXml, xml, "Test Logout request xml parameters"); |
| 139 | + |
| 140 | + } |
| 141 | + |
| 142 | + private static string GetIssuerInstant(string xml, string name) |
| 143 | + { |
| 144 | + XmlDocument doc = SamlAssertionUtils.CanonicalizeXml(xml); |
| 145 | + XmlNodeList nodeList = doc.GetElementsByTagName(name); |
| 146 | + string issuer = nodeList[0].Attributes.GetNamedItem("IssueInstant").Value; |
| 147 | + return issuer; |
| 148 | + |
| 149 | + } |
| 150 | + |
| 151 | + private bool VerifySignature_internal(string certPath, Dictionary<string, string> redirectMessage) |
| 152 | + { |
| 153 | + |
| 154 | + byte[] signature = GamSaml20.Utils.Encoding.DecodeParameter(redirectMessage["Signature"]); |
| 155 | + string value; |
| 156 | + string signedMessage; |
| 157 | + if (redirectMessage.TryGetValue("RelayState", out value)) |
| 158 | + { |
| 159 | + signedMessage = $"SAMLRequest={redirectMessage["SAMLRequest"]}"; |
| 160 | + signedMessage += $"&RelayState={redirectMessage["RelayState"]}"; |
| 161 | + signedMessage += $"&SigAlg={redirectMessage["SigAlg"]}"; |
| 162 | + } |
| 163 | + else |
| 164 | + { |
| 165 | + signedMessage = $"SAMLRequest={redirectMessage["SAMLRequest"]}"; |
| 166 | + signedMessage += $"&SigAlg={redirectMessage["SigAlg"]}"; |
| 167 | + } |
| 168 | + |
| 169 | + byte[] query = System.Text.Encoding.UTF8.GetBytes(signedMessage); |
| 170 | + try |
| 171 | + { |
| 172 | + RSACryptoServiceProvider csp = Keys.GetPublicRSACryptoServiceProvider(certPath); |
| 173 | + |
| 174 | + if (csp == null) |
| 175 | + { |
| 176 | + Assert.Fail("VerifySignature_internal logout RSACryptoServiceProvider is null"); |
| 177 | + } |
| 178 | + string sigalg = HttpUtility.UrlDecode(redirectMessage["SigAlg"]); |
| 179 | + GamSaml20.Utils.Hash hash = HashUtils.GetHashFromSigAlg(sigalg); |
| 180 | + |
| 181 | + return csp.VerifyData(query, CryptoConfig.MapNameToOID(HashUtils.ValueOf(hash)), signature); |
| 182 | + } |
| 183 | + catch (Exception e) |
| 184 | + { |
| 185 | + Assert.Fail("VerifySignature_internal", e); |
| 186 | + return false; |
| 187 | + } |
| 188 | + } |
| 189 | + |
| 190 | + private void TestRequestSignature(Dictionary<string, string> redirectMessage, string function) |
| 191 | + { |
| 192 | + bool verifies = VerifySignature_internal(Path.Combine(resources, "saml20", "javacert.crt"), redirectMessage); |
| 193 | + Assert.IsTrue(verifies, $"Test {function} request signature"); |
| 194 | + } |
| 195 | + |
| 196 | + private void TestRequestParameters(Dictionary<string, string> redirectMessage, string function) |
| 197 | + { |
| 198 | + string relayState = DecodeParm(redirectMessage["RelayState"]); |
| 199 | + Assert.AreEqual("http://relaystate.com", relayState, $"Test {function} parameters RelayState"); |
| 200 | + string sigAlg = DecodeParm(redirectMessage["SigAlg"]); |
| 201 | + Assert.AreEqual("http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", sigAlg, $"Test {function} request parameters SigAlg"); |
| 202 | + } |
| 203 | + |
| 204 | + private static string DecodeParm(string parm) |
| 205 | + { |
| 206 | + try |
| 207 | + { |
| 208 | + return HttpUtility.UrlDecode(parm); |
| 209 | + } |
| 210 | + catch (Exception e) |
| 211 | + { |
| 212 | + Console.WriteLine(e.StackTrace); |
| 213 | + return ""; |
| 214 | + } |
| 215 | + } |
| 216 | + |
| 217 | + private static string GetQueryString(string samlRequest) |
| 218 | + { |
| 219 | + try |
| 220 | + { |
| 221 | + string[] uri = samlRequest.Split('?'); |
| 222 | + return uri[1]; |
| 223 | + } |
| 224 | + catch (Exception e) |
| 225 | + { |
| 226 | + Console.WriteLine(e.StackTrace); |
| 227 | + return ""; |
| 228 | + } |
| 229 | + } |
| 230 | + |
| 231 | + private static SamlParms CreateParameters() |
| 232 | + { |
| 233 | + SamlParms parms = new SamlParms(); |
| 234 | + parms.CertPath = Path.Combine(resources, "saml20", "mykeystore.pfx"); |
| 235 | + parms.CertPass = password; |
| 236 | + parms.Acs = "http://myapp.com/acs"; |
| 237 | + parms.ForceAuthn = false; |
| 238 | + parms.ServiceProviderEntityID = "SPEntityID"; |
| 239 | + parms.IdentityProviderEntityID = "EntityID"; |
| 240 | + parms.PolicyFormat = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"; |
| 241 | + parms.AuthContext = "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport"; |
| 242 | + parms.EndPointLocation = "http://endpoint/saml"; |
| 243 | + parms.Id = "_idtralala"; |
| 244 | + parms.SingleLogoutEndpoint = "http://idp.com/slo"; |
| 245 | + parms.SessionIndex = "123456789"; |
| 246 | + parms.NameID = "nameID"; |
| 247 | + return parms; |
| 248 | + } |
| 249 | + |
| 250 | + private static Dictionary<string, string> ParseRedirect(string request) |
| 251 | + { |
| 252 | + Dictionary<string, string> result = new Dictionary<string, string>(); |
| 253 | + string[] redirect = request.Split('&'); |
| 254 | + |
| 255 | + foreach (string s in redirect) |
| 256 | + { |
| 257 | + string[] res = s.Split('='); |
| 258 | + result[res[0]] = res[1]; |
| 259 | + } |
| 260 | + return result; |
| 261 | + } |
| 262 | + |
| 263 | + private static string GetStartupDirectory() |
| 264 | + { |
| 265 | +#pragma warning disable SYSLIB0044 |
| 266 | + string dir = Assembly.GetCallingAssembly().GetName().CodeBase; |
| 267 | +#pragma warning restore SYSLIB0044 |
| 268 | + Uri uri = new Uri(dir); |
| 269 | + return Path.GetDirectoryName(uri.LocalPath); |
| 270 | + } |
| 271 | + } |
| 272 | +} |
0 commit comments