1 // Copyright 2011 The Chromium Authors
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/functional/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 std::u16string & combined,const std::u16string & expected_domain,const std::u16string & expected_user)31 void MatchDomainUserAfterSplit(const std::u16string& combined,
32 const std::u16string& expected_domain,
33 const std::u16string& expected_user) {
34 std::u16string actual_domain;
35 std::u16string 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(u"foobar", u"", u"foobar");
53 MatchDomainUserAfterSplit(u"FOO\\bar", u"FOO", u"bar");
54 }
55
TEST(HttpAuthSSPITest,DetermineMaxTokenLength_Normal)56 TEST(HttpAuthSSPITest, DetermineMaxTokenLength_Normal) {
57 SecPkgInfoW package_info;
58 memset(&package_info, 0x0, sizeof(package_info));
59 package_info.cbMaxToken = 1337;
60
61 MockSSPILibrary mock_library{L"NTLM"};
62 mock_library.ExpectQuerySecurityPackageInfo(SEC_E_OK, &package_info);
63 ULONG max_token_length = kMaxTokenLength;
64 int rv = mock_library.DetermineMaxTokenLength(&max_token_length);
65 EXPECT_THAT(rv, IsOk());
66 EXPECT_EQ(1337u, max_token_length);
67 }
68
TEST(HttpAuthSSPITest,DetermineMaxTokenLength_InvalidPackage)69 TEST(HttpAuthSSPITest, DetermineMaxTokenLength_InvalidPackage) {
70 MockSSPILibrary mock_library{L"Foo"};
71 mock_library.ExpectQuerySecurityPackageInfo(SEC_E_SECPKG_NOT_FOUND, nullptr);
72 ULONG max_token_length = kMaxTokenLength;
73 int rv = mock_library.DetermineMaxTokenLength(&max_token_length);
74 EXPECT_THAT(rv, IsError(ERR_UNSUPPORTED_AUTH_SCHEME));
75 // |DetermineMaxTokenLength()| interface states that |max_token_length| should
76 // not change on failure.
77 EXPECT_EQ(100u, max_token_length);
78 }
79
TEST(HttpAuthSSPITest,ParseChallenge_FirstRound)80 TEST(HttpAuthSSPITest, ParseChallenge_FirstRound) {
81 // The first round should just consist of an unadorned "Negotiate" header.
82 MockSSPILibrary mock_library{NEGOSSP_NAME};
83 HttpAuthSSPI auth_sspi(&mock_library, HttpAuth::AUTH_SCHEME_NEGOTIATE);
84 std::string challenge_text = "Negotiate";
85 HttpAuthChallengeTokenizer challenge(challenge_text.begin(),
86 challenge_text.end());
87 EXPECT_EQ(HttpAuth::AUTHORIZATION_RESULT_ACCEPT,
88 auth_sspi.ParseChallenge(&challenge));
89 }
90
TEST(HttpAuthSSPITest,ParseChallenge_TwoRounds)91 TEST(HttpAuthSSPITest, ParseChallenge_TwoRounds) {
92 // The first round should just have "Negotiate", and the second round should
93 // have a valid base64 token associated with it.
94 MockSSPILibrary mock_library{NEGOSSP_NAME};
95 HttpAuthSSPI auth_sspi(&mock_library, HttpAuth::AUTH_SCHEME_NEGOTIATE);
96 std::string first_challenge_text = "Negotiate";
97 HttpAuthChallengeTokenizer first_challenge(first_challenge_text.begin(),
98 first_challenge_text.end());
99 EXPECT_EQ(HttpAuth::AUTHORIZATION_RESULT_ACCEPT,
100 auth_sspi.ParseChallenge(&first_challenge));
101
102 // Generate an auth token and create another thing.
103 std::string auth_token;
104 EXPECT_EQ(OK,
105 auth_sspi.GenerateAuthToken(
106 nullptr, "HTTP/intranet.google.com", std::string(), &auth_token,
107 NetLogWithSource(), base::BindOnce(&UnexpectedCallback)));
108
109 std::string second_challenge_text = "Negotiate Zm9vYmFy";
110 HttpAuthChallengeTokenizer second_challenge(second_challenge_text.begin(),
111 second_challenge_text.end());
112 EXPECT_EQ(HttpAuth::AUTHORIZATION_RESULT_ACCEPT,
113 auth_sspi.ParseChallenge(&second_challenge));
114 }
115
TEST(HttpAuthSSPITest,ParseChallenge_UnexpectedTokenFirstRound)116 TEST(HttpAuthSSPITest, ParseChallenge_UnexpectedTokenFirstRound) {
117 // If the first round challenge has an additional authentication token, it
118 // should be treated as an invalid challenge from the server.
119 MockSSPILibrary mock_library{NEGOSSP_NAME};
120 HttpAuthSSPI auth_sspi(&mock_library, HttpAuth::AUTH_SCHEME_NEGOTIATE);
121 std::string challenge_text = "Negotiate Zm9vYmFy";
122 HttpAuthChallengeTokenizer challenge(challenge_text.begin(),
123 challenge_text.end());
124 EXPECT_EQ(HttpAuth::AUTHORIZATION_RESULT_INVALID,
125 auth_sspi.ParseChallenge(&challenge));
126 }
127
TEST(HttpAuthSSPITest,ParseChallenge_MissingTokenSecondRound)128 TEST(HttpAuthSSPITest, ParseChallenge_MissingTokenSecondRound) {
129 // If a later-round challenge is simply "Negotiate", it should be treated as
130 // an authentication challenge rejection from the server or proxy.
131 MockSSPILibrary mock_library{NEGOSSP_NAME};
132 HttpAuthSSPI auth_sspi(&mock_library, HttpAuth::AUTH_SCHEME_NEGOTIATE);
133 std::string first_challenge_text = "Negotiate";
134 HttpAuthChallengeTokenizer first_challenge(first_challenge_text.begin(),
135 first_challenge_text.end());
136 EXPECT_EQ(HttpAuth::AUTHORIZATION_RESULT_ACCEPT,
137 auth_sspi.ParseChallenge(&first_challenge));
138
139 std::string auth_token;
140 EXPECT_EQ(OK,
141 auth_sspi.GenerateAuthToken(
142 nullptr, "HTTP/intranet.google.com", std::string(), &auth_token,
143 NetLogWithSource(), base::BindOnce(&UnexpectedCallback)));
144 std::string second_challenge_text = "Negotiate";
145 HttpAuthChallengeTokenizer second_challenge(second_challenge_text.begin(),
146 second_challenge_text.end());
147 EXPECT_EQ(HttpAuth::AUTHORIZATION_RESULT_REJECT,
148 auth_sspi.ParseChallenge(&second_challenge));
149 }
150
TEST(HttpAuthSSPITest,ParseChallenge_NonBase64EncodedToken)151 TEST(HttpAuthSSPITest, ParseChallenge_NonBase64EncodedToken) {
152 // If a later-round challenge has an invalid base64 encoded token, it should
153 // be treated as an invalid challenge.
154 MockSSPILibrary mock_library{NEGOSSP_NAME};
155 HttpAuthSSPI auth_sspi(&mock_library, HttpAuth::AUTH_SCHEME_NEGOTIATE);
156 std::string first_challenge_text = "Negotiate";
157 HttpAuthChallengeTokenizer first_challenge(first_challenge_text.begin(),
158 first_challenge_text.end());
159 EXPECT_EQ(HttpAuth::AUTHORIZATION_RESULT_ACCEPT,
160 auth_sspi.ParseChallenge(&first_challenge));
161
162 std::string auth_token;
163 EXPECT_EQ(OK,
164 auth_sspi.GenerateAuthToken(
165 nullptr, "HTTP/intranet.google.com", std::string(), &auth_token,
166 NetLogWithSource(), base::BindOnce(&UnexpectedCallback)));
167 std::string second_challenge_text = "Negotiate =happyjoy=";
168 HttpAuthChallengeTokenizer second_challenge(second_challenge_text.begin(),
169 second_challenge_text.end());
170 EXPECT_EQ(HttpAuth::AUTHORIZATION_RESULT_INVALID,
171 auth_sspi.ParseChallenge(&second_challenge));
172 }
173
174 // Runs through a full handshake against the MockSSPILibrary.
TEST(HttpAuthSSPITest,GenerateAuthToken_FullHandshake_AmbientCreds)175 TEST(HttpAuthSSPITest, GenerateAuthToken_FullHandshake_AmbientCreds) {
176 MockSSPILibrary mock_library{NEGOSSP_NAME};
177 HttpAuthSSPI auth_sspi(&mock_library, HttpAuth::AUTH_SCHEME_NEGOTIATE);
178 std::string first_challenge_text = "Negotiate";
179 HttpAuthChallengeTokenizer first_challenge(first_challenge_text.begin(),
180 first_challenge_text.end());
181 ASSERT_EQ(HttpAuth::AUTHORIZATION_RESULT_ACCEPT,
182 auth_sspi.ParseChallenge(&first_challenge));
183
184 std::string auth_token;
185 ASSERT_EQ(OK,
186 auth_sspi.GenerateAuthToken(
187 nullptr, "HTTP/intranet.google.com", std::string(), &auth_token,
188 NetLogWithSource(), base::BindOnce(&UnexpectedCallback)));
189 EXPECT_EQ("Negotiate ", auth_token.substr(0, 10));
190
191 std::string decoded_token;
192 ASSERT_TRUE(base::Base64Decode(auth_token.substr(10), &decoded_token));
193
194 // This token string indicates that HttpAuthSSPI correctly established the
195 // security context using the default credentials.
196 EXPECT_EQ("<Default>'s token #1 for HTTP/intranet.google.com", decoded_token);
197
198 // The server token is arbitrary.
199 std::string second_challenge_text = "Negotiate UmVzcG9uc2U=";
200 HttpAuthChallengeTokenizer second_challenge(second_challenge_text.begin(),
201 second_challenge_text.end());
202 ASSERT_EQ(HttpAuth::AUTHORIZATION_RESULT_ACCEPT,
203 auth_sspi.ParseChallenge(&second_challenge));
204
205 ASSERT_EQ(OK,
206 auth_sspi.GenerateAuthToken(
207 nullptr, "HTTP/intranet.google.com", std::string(), &auth_token,
208 NetLogWithSource(), base::BindOnce(&UnexpectedCallback)));
209 ASSERT_EQ("Negotiate ", auth_token.substr(0, 10));
210 ASSERT_TRUE(base::Base64Decode(auth_token.substr(10), &decoded_token));
211 EXPECT_EQ("<Default>'s token #2 for HTTP/intranet.google.com", decoded_token);
212 }
213
214 // Test NetLogs produced while going through a full Negotiate handshake.
TEST(HttpAuthSSPITest,GenerateAuthToken_FullHandshake_AmbientCreds_Logging)215 TEST(HttpAuthSSPITest, GenerateAuthToken_FullHandshake_AmbientCreds_Logging) {
216 RecordingNetLogObserver net_log_observer;
217 NetLogWithSource net_log_with_source =
218 NetLogWithSource::Make(NetLogSourceType::NONE);
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_with_source, 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_with_source, base::BindOnce(&UnexpectedCallback)));
243
244 auto entries = net_log_observer.GetEntriesWithType(
245 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 = net_log_observer.GetEntriesWithType(
258 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