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