• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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