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