1 // Copyright (c) 2011 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 #include "net/http/http_auth_sspi_win.h"
6 
7 #include <vector>
8 
9 #include "base/base64.h"
10 #include "base/bind.h"
11 #include "base/json/json_reader.h"
12 #include "net/base/net_errors.h"
13 #include "net/http/http_auth.h"
14 #include "net/http/http_auth_challenge_tokenizer.h"
15 #include "net/http/mock_sspi_library_win.h"
16 #include "net/log/net_log_entry.h"
17 #include "net/log/net_log_event_type.h"
18 #include "net/log/net_log_with_source.h"
19 #include "net/log/test_net_log.h"
20 #include "net/test/gtest_util.h"
21 #include "testing/gmock/include/gmock/gmock.h"
22 #include "testing/gtest/include/gtest/gtest.h"
23 
24 using net::test::IsError;
25 using net::test::IsOk;
26 
27 namespace net {
28 
29 namespace {
30 
MatchDomainUserAfterSplit(const base::string16 & combined,const base::string16 & expected_domain,const base::string16 & expected_user)31 void MatchDomainUserAfterSplit(const base::string16& combined,
32                                const base::string16& expected_domain,
33                                const base::string16& expected_user) {
34   base::string16 actual_domain;
35   base::string16 actual_user;
36   SplitDomainAndUser(combined, &actual_domain, &actual_user);
37   EXPECT_EQ(expected_domain, actual_domain);
38   EXPECT_EQ(expected_user, actual_user);
39 }
40 
41 const ULONG kMaxTokenLength = 100;
42 
UnexpectedCallback(int result)43 void UnexpectedCallback(int result) {
44   // At present getting tokens from gssapi is fully synchronous, so the callback
45   // should never be called.
46   ADD_FAILURE();
47 }
48 
49 }  // namespace
50 
TEST(HttpAuthSSPITest,SplitUserAndDomain)51 TEST(HttpAuthSSPITest, SplitUserAndDomain) {
52   MatchDomainUserAfterSplit(STRING16_LITERAL("foobar"), STRING16_LITERAL(""),
53                             STRING16_LITERAL("foobar"));
54   MatchDomainUserAfterSplit(STRING16_LITERAL("FOO\\bar"),
55                             STRING16_LITERAL("FOO"), STRING16_LITERAL("bar"));
56 }
57 
TEST(HttpAuthSSPITest,DetermineMaxTokenLength_Normal)58 TEST(HttpAuthSSPITest, DetermineMaxTokenLength_Normal) {
59   SecPkgInfoW package_info;
60   memset(&package_info, 0x0, sizeof(package_info));
61   package_info.cbMaxToken = 1337;
62 
63   MockSSPILibrary mock_library{L"NTLM"};
64   mock_library.ExpectQuerySecurityPackageInfo(SEC_E_OK, &package_info);
65   ULONG max_token_length = kMaxTokenLength;
66   int rv = mock_library.DetermineMaxTokenLength(&max_token_length);
67   EXPECT_THAT(rv, IsOk());
68   EXPECT_EQ(1337u, max_token_length);
69 }
70 
TEST(HttpAuthSSPITest,DetermineMaxTokenLength_InvalidPackage)71 TEST(HttpAuthSSPITest, DetermineMaxTokenLength_InvalidPackage) {
72   MockSSPILibrary mock_library{L"Foo"};
73   mock_library.ExpectQuerySecurityPackageInfo(SEC_E_SECPKG_NOT_FOUND, nullptr);
74   ULONG max_token_length = kMaxTokenLength;
75   int rv = mock_library.DetermineMaxTokenLength(&max_token_length);
76   EXPECT_THAT(rv, IsError(ERR_UNSUPPORTED_AUTH_SCHEME));
77   // |DetermineMaxTokenLength()| interface states that |max_token_length| should
78   // not change on failure.
79   EXPECT_EQ(100u, max_token_length);
80 }
81 
TEST(HttpAuthSSPITest,ParseChallenge_FirstRound)82 TEST(HttpAuthSSPITest, ParseChallenge_FirstRound) {
83   // The first round should just consist of an unadorned "Negotiate" header.
84   MockSSPILibrary mock_library{NEGOSSP_NAME};
85   HttpAuthSSPI auth_sspi(&mock_library, HttpAuth::AUTH_SCHEME_NEGOTIATE);
86   std::string challenge_text = "Negotiate";
87   HttpAuthChallengeTokenizer challenge(challenge_text.begin(),
88                                        challenge_text.end());
89   EXPECT_EQ(HttpAuth::AUTHORIZATION_RESULT_ACCEPT,
90             auth_sspi.ParseChallenge(&challenge));
91 }
92 
TEST(HttpAuthSSPITest,ParseChallenge_TwoRounds)93 TEST(HttpAuthSSPITest, ParseChallenge_TwoRounds) {
94   // The first round should just have "Negotiate", and the second round should
95   // have a valid base64 token associated with it.
96   MockSSPILibrary mock_library{NEGOSSP_NAME};
97   HttpAuthSSPI auth_sspi(&mock_library, HttpAuth::AUTH_SCHEME_NEGOTIATE);
98   std::string first_challenge_text = "Negotiate";
99   HttpAuthChallengeTokenizer first_challenge(first_challenge_text.begin(),
100                                              first_challenge_text.end());
101   EXPECT_EQ(HttpAuth::AUTHORIZATION_RESULT_ACCEPT,
102             auth_sspi.ParseChallenge(&first_challenge));
103 
104   // Generate an auth token and create another thing.
105   std::string auth_token;
106   EXPECT_EQ(OK,
107             auth_sspi.GenerateAuthToken(
108                 nullptr, "HTTP/intranet.google.com", std::string(), &auth_token,
109                 NetLogWithSource(), base::BindOnce(&UnexpectedCallback)));
110 
111   std::string second_challenge_text = "Negotiate Zm9vYmFy";
112   HttpAuthChallengeTokenizer second_challenge(second_challenge_text.begin(),
113                                               second_challenge_text.end());
114   EXPECT_EQ(HttpAuth::AUTHORIZATION_RESULT_ACCEPT,
115             auth_sspi.ParseChallenge(&second_challenge));
116 }
117 
TEST(HttpAuthSSPITest,ParseChallenge_UnexpectedTokenFirstRound)118 TEST(HttpAuthSSPITest, ParseChallenge_UnexpectedTokenFirstRound) {
119   // If the first round challenge has an additional authentication token, it
120   // should be treated as an invalid challenge from the server.
121   MockSSPILibrary mock_library{NEGOSSP_NAME};
122   HttpAuthSSPI auth_sspi(&mock_library, HttpAuth::AUTH_SCHEME_NEGOTIATE);
123   std::string challenge_text = "Negotiate Zm9vYmFy";
124   HttpAuthChallengeTokenizer challenge(challenge_text.begin(),
125                                        challenge_text.end());
126   EXPECT_EQ(HttpAuth::AUTHORIZATION_RESULT_INVALID,
127             auth_sspi.ParseChallenge(&challenge));
128 }
129 
TEST(HttpAuthSSPITest,ParseChallenge_MissingTokenSecondRound)130 TEST(HttpAuthSSPITest, ParseChallenge_MissingTokenSecondRound) {
131   // If a later-round challenge is simply "Negotiate", it should be treated as
132   // an authentication challenge rejection from the server or proxy.
133   MockSSPILibrary mock_library{NEGOSSP_NAME};
134   HttpAuthSSPI auth_sspi(&mock_library, HttpAuth::AUTH_SCHEME_NEGOTIATE);
135   std::string first_challenge_text = "Negotiate";
136   HttpAuthChallengeTokenizer first_challenge(first_challenge_text.begin(),
137                                              first_challenge_text.end());
138   EXPECT_EQ(HttpAuth::AUTHORIZATION_RESULT_ACCEPT,
139             auth_sspi.ParseChallenge(&first_challenge));
140 
141   std::string auth_token;
142   EXPECT_EQ(OK,
143             auth_sspi.GenerateAuthToken(
144                 nullptr, "HTTP/intranet.google.com", std::string(), &auth_token,
145                 NetLogWithSource(), base::BindOnce(&UnexpectedCallback)));
146   std::string second_challenge_text = "Negotiate";
147   HttpAuthChallengeTokenizer second_challenge(second_challenge_text.begin(),
148                                               second_challenge_text.end());
149   EXPECT_EQ(HttpAuth::AUTHORIZATION_RESULT_REJECT,
150             auth_sspi.ParseChallenge(&second_challenge));
151 }
152 
TEST(HttpAuthSSPITest,ParseChallenge_NonBase64EncodedToken)153 TEST(HttpAuthSSPITest, ParseChallenge_NonBase64EncodedToken) {
154   // If a later-round challenge has an invalid base64 encoded token, it should
155   // be treated as an invalid challenge.
156   MockSSPILibrary mock_library{NEGOSSP_NAME};
157   HttpAuthSSPI auth_sspi(&mock_library, HttpAuth::AUTH_SCHEME_NEGOTIATE);
158   std::string first_challenge_text = "Negotiate";
159   HttpAuthChallengeTokenizer first_challenge(first_challenge_text.begin(),
160                                              first_challenge_text.end());
161   EXPECT_EQ(HttpAuth::AUTHORIZATION_RESULT_ACCEPT,
162             auth_sspi.ParseChallenge(&first_challenge));
163 
164   std::string auth_token;
165   EXPECT_EQ(OK,
166             auth_sspi.GenerateAuthToken(
167                 nullptr, "HTTP/intranet.google.com", std::string(), &auth_token,
168                 NetLogWithSource(), base::BindOnce(&UnexpectedCallback)));
169   std::string second_challenge_text = "Negotiate =happyjoy=";
170   HttpAuthChallengeTokenizer second_challenge(second_challenge_text.begin(),
171                                               second_challenge_text.end());
172   EXPECT_EQ(HttpAuth::AUTHORIZATION_RESULT_INVALID,
173             auth_sspi.ParseChallenge(&second_challenge));
174 }
175 
176 // Runs through a full handshake against the MockSSPILibrary.
TEST(HttpAuthSSPITest,GenerateAuthToken_FullHandshake_AmbientCreds)177 TEST(HttpAuthSSPITest, GenerateAuthToken_FullHandshake_AmbientCreds) {
178   MockSSPILibrary mock_library{NEGOSSP_NAME};
179   HttpAuthSSPI auth_sspi(&mock_library, HttpAuth::AUTH_SCHEME_NEGOTIATE);
180   std::string first_challenge_text = "Negotiate";
181   HttpAuthChallengeTokenizer first_challenge(first_challenge_text.begin(),
182                                              first_challenge_text.end());
183   ASSERT_EQ(HttpAuth::AUTHORIZATION_RESULT_ACCEPT,
184             auth_sspi.ParseChallenge(&first_challenge));
185 
186   std::string auth_token;
187   ASSERT_EQ(OK,
188             auth_sspi.GenerateAuthToken(
189                 nullptr, "HTTP/intranet.google.com", std::string(), &auth_token,
190                 NetLogWithSource(), base::BindOnce(&UnexpectedCallback)));
191   EXPECT_EQ("Negotiate ", auth_token.substr(0, 10));
192 
193   std::string decoded_token;
194   ASSERT_TRUE(base::Base64Decode(auth_token.substr(10), &decoded_token));
195 
196   // This token string indicates that HttpAuthSSPI correctly established the
197   // security context using the default credentials.
198   EXPECT_EQ("<Default>'s token #1 for HTTP/intranet.google.com", decoded_token);
199 
200   // The server token is arbitrary.
201   std::string second_challenge_text = "Negotiate UmVzcG9uc2U=";
202   HttpAuthChallengeTokenizer second_challenge(second_challenge_text.begin(),
203                                               second_challenge_text.end());
204   ASSERT_EQ(HttpAuth::AUTHORIZATION_RESULT_ACCEPT,
205             auth_sspi.ParseChallenge(&second_challenge));
206 
207   ASSERT_EQ(OK,
208             auth_sspi.GenerateAuthToken(
209                 nullptr, "HTTP/intranet.google.com", std::string(), &auth_token,
210                 NetLogWithSource(), base::BindOnce(&UnexpectedCallback)));
211   ASSERT_EQ("Negotiate ", auth_token.substr(0, 10));
212   ASSERT_TRUE(base::Base64Decode(auth_token.substr(10), &decoded_token));
213   EXPECT_EQ("<Default>'s token #2 for HTTP/intranet.google.com", decoded_token);
214 }
215 
216 // Test NetLogs produced while going through a full Negotiate handshake.
TEST(HttpAuthSSPITest,GenerateAuthToken_FullHandshake_AmbientCreds_Logging)217 TEST(HttpAuthSSPITest, GenerateAuthToken_FullHandshake_AmbientCreds_Logging) {
218   RecordingBoundTestNetLog net_log;
219   MockSSPILibrary mock_library{NEGOSSP_NAME};
220   HttpAuthSSPI auth_sspi(&mock_library, HttpAuth::AUTH_SCHEME_NEGOTIATE);
221   std::string first_challenge_text = "Negotiate";
222   HttpAuthChallengeTokenizer first_challenge(first_challenge_text.begin(),
223                                              first_challenge_text.end());
224   ASSERT_EQ(HttpAuth::AUTHORIZATION_RESULT_ACCEPT,
225             auth_sspi.ParseChallenge(&first_challenge));
226 
227   std::string auth_token;
228   ASSERT_EQ(OK,
229             auth_sspi.GenerateAuthToken(
230                 nullptr, "HTTP/intranet.google.com", std::string(), &auth_token,
231                 net_log.bound(), base::BindOnce(&UnexpectedCallback)));
232 
233   // The token is the ASCII string "Response" in base64.
234   std::string second_challenge_text = "Negotiate UmVzcG9uc2U=";
235   HttpAuthChallengeTokenizer second_challenge(second_challenge_text.begin(),
236                                               second_challenge_text.end());
237   ASSERT_EQ(HttpAuth::AUTHORIZATION_RESULT_ACCEPT,
238             auth_sspi.ParseChallenge(&second_challenge));
239   ASSERT_EQ(OK,
240             auth_sspi.GenerateAuthToken(
241                 nullptr, "HTTP/intranet.google.com", std::string(), &auth_token,
242                 net_log.bound(), base::BindOnce(&UnexpectedCallback)));
243 
244   auto entries =
245       net_log.GetEntriesWithType(NetLogEventType::AUTH_LIBRARY_ACQUIRE_CREDS);
246   ASSERT_EQ(2u, entries.size());  // BEGIN and END.
247   auto expected = base::JSONReader::Read(R"(
248     {
249       "status": {
250         "net_error": 0,
251         "security_status": 0
252        }
253     }
254   )");
255   EXPECT_EQ(expected, entries[1].params);
256 
257   entries =
258       net_log.GetEntriesWithType(NetLogEventType::AUTH_LIBRARY_INIT_SEC_CTX);
259   ASSERT_EQ(4u, entries.size());
260 
261   expected = base::JSONReader::Read(R"(
262     {
263        "flags": {
264           "delegated": false,
265           "mutual": false,
266           "value": "0x00000000"
267        },
268        "spn": "HTTP/intranet.google.com"
269     }
270   )");
271   EXPECT_EQ(expected, entries[0].params);
272 
273   expected = base::JSONReader::Read(R"(
274     {
275       "context": {
276          "authority": "Dodgy Server",
277          "flags": {
278             "delegated": false,
279             "mutual": false,
280             "value": "0x00000000"
281          },
282          "mechanism": "Itsa me Kerberos!!",
283          "open": true,
284          "source": "\u003CDefault>",
285          "target": "HTTP/intranet.google.com"
286       },
287       "status": {
288          "net_error": 0,
289          "security_status": 0
290       }
291     }
292   )");
293   EXPECT_EQ(expected, entries[1].params);
294 
295   expected = base::JSONReader::Read(R"(
296     {
297       "context": {
298         "authority": "Dodgy Server",
299         "flags": {
300            "delegated": false,
301            "mutual": false,
302            "value": "0x00000000"
303         },
304         "mechanism": "Itsa me Kerberos!!",
305         "open": false,
306         "source": "\u003CDefault>",
307         "target": "HTTP/intranet.google.com"
308       },
309       "status": {
310          "net_error": 0,
311          "security_status": 0
312       }
313     }
314   )");
315   EXPECT_EQ(expected, entries[3].params);
316 }
317 }  // namespace net
318