xref: /aosp_15_r20/external/cronet/net/http/http_auth_sspi_win_unittest.cc (revision 6777b5387eb2ff775bb5750e3f5d96f37fb7352b)
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