1 // Copyright (c) 2013 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.
5 #include "google_apis/gaia/fake_gaia.h"
9 #include "base/base_paths.h"
10 #include "base/bind.h"
11 #include "base/bind_helpers.h"
12 #include "base/file_util.h"
13 #include "base/files/file_path.h"
14 #include "base/json/json_writer.h"
15 #include "base/logging.h"
16 #include "base/memory/linked_ptr.h"
17 #include "base/path_service.h"
18 #include "base/strings/string_number_conversions.h"
19 #include "base/strings/string_split.h"
20 #include "base/strings/string_util.h"
21 #include "base/strings/stringprintf.h"
22 #include "base/values.h"
23 #include "google_apis/gaia/gaia_constants.h"
24 #include "google_apis/gaia/gaia_urls.h"
25 #include "net/base/url_util.h"
26 #include "net/cookies/parsed_cookie.h"
27 #include "net/http/http_status_code.h"
28 #include "net/test/embedded_test_server/http_request.h"
29 #include "net/test/embedded_test_server/http_response.h"
30 #include "url/url_parse.h"
32 #define REGISTER_RESPONSE_HANDLER(url, method) \
33 request_handlers_.insert(std::make_pair( \
34 url.path(), base::Bind(&FakeGaia::method, base::Unretained(this))))
36 #define REGISTER_PATH_RESPONSE_HANDLER(path, method) \
37 request_handlers_.insert(std::make_pair( \
38 path, base::Bind(&FakeGaia::method, base::Unretained(this))))
40 using namespace net::test_server
;
44 const base::FilePath::CharType kServiceLogin
[] =
45 FILE_PATH_LITERAL("google_apis/test/service_login.html");
47 // OAuth2 Authentication header value prefix.
48 const char kAuthHeaderBearer
[] = "Bearer ";
49 const char kAuthHeaderOAuth
[] = "OAuth ";
51 const char kListAccountsResponseFormat
[] =
52 "[\"gaia.l.a.r\",[[\"gaia.l.a\",1,\"\",\"%s\",\"\",1,1,0]]]";
54 typedef std::map
<std::string
, std::string
> CookieMap
;
56 // Parses cookie name-value map our of |request|.
57 CookieMap
GetRequestCookies(const HttpRequest
& request
) {
59 std::map
<std::string
, std::string
>::const_iterator iter
=
60 request
.headers
.find("Cookie");
61 if (iter
!= request
.headers
.end()) {
62 std::vector
<std::string
> cookie_nv_pairs
;
63 base::SplitString(iter
->second
, ' ', &cookie_nv_pairs
);
64 for(std::vector
<std::string
>::const_iterator cookie_line
=
65 cookie_nv_pairs
.begin();
66 cookie_line
!= cookie_nv_pairs
.end();
68 std::vector
<std::string
> name_value
;
69 base::SplitString(*cookie_line
, '=', &name_value
);
70 if (name_value
.size() != 2)
73 std::string value
= name_value
[1];
74 if (value
.size() && value
[value
.size() - 1] == ';')
75 value
= value
.substr(0, value
.size() -1);
77 result
.insert(std::make_pair(name_value
[0], value
));
83 // Extracts the |access_token| from authorization header of |request|.
84 bool GetAccessToken(const HttpRequest
& request
,
85 const char* auth_token_prefix
,
86 std::string
* access_token
) {
87 std::map
<std::string
, std::string
>::const_iterator auth_header_entry
=
88 request
.headers
.find("Authorization");
89 if (auth_header_entry
!= request
.headers
.end()) {
90 if (StartsWithASCII(auth_header_entry
->second
, auth_token_prefix
, true)) {
91 *access_token
= auth_header_entry
->second
.substr(
92 strlen(auth_token_prefix
));
100 void SetCookies(BasicHttpResponse
* http_response
,
101 const std::string
& sid_cookie
,
102 const std::string
& lsid_cookie
) {
103 http_response
->AddCustomHeader(
105 base::StringPrintf("SID=%s; Path=/; HttpOnly", sid_cookie
.c_str()));
106 http_response
->AddCustomHeader(
108 base::StringPrintf("LSID=%s; Path=/; HttpOnly", lsid_cookie
.c_str()));
113 FakeGaia::AccessTokenInfo::AccessTokenInfo()
114 : expires_in(3600) {}
116 FakeGaia::AccessTokenInfo::~AccessTokenInfo() {}
118 FakeGaia::MergeSessionParams::MergeSessionParams() {
121 FakeGaia::MergeSessionParams::~MergeSessionParams() {
124 FakeGaia::FakeGaia() {
125 base::FilePath source_root_dir
;
126 PathService::Get(base::DIR_SOURCE_ROOT
, &source_root_dir
);
127 CHECK(base::ReadFileToString(
128 source_root_dir
.Append(base::FilePath(kServiceLogin
)),
129 &service_login_response_
));
132 FakeGaia::~FakeGaia() {}
134 void FakeGaia::SetMergeSessionParams(
135 const MergeSessionParams
& params
) {
136 merge_session_params_
= params
;
139 void FakeGaia::Initialize() {
140 GaiaUrls
* gaia_urls
= GaiaUrls::GetInstance();
141 // Handles /MergeSession GAIA call.
142 REGISTER_RESPONSE_HANDLER(
143 gaia_urls
->merge_session_url(), HandleMergeSession
);
145 // Handles /o/oauth2/programmatic_auth GAIA call.
146 REGISTER_RESPONSE_HANDLER(
147 gaia_urls
->client_login_to_oauth2_url(), HandleProgramaticAuth
);
149 // Handles /ServiceLogin GAIA call.
150 REGISTER_RESPONSE_HANDLER(
151 gaia_urls
->service_login_url(), HandleServiceLogin
);
153 // Handles /OAuthLogin GAIA call.
154 REGISTER_RESPONSE_HANDLER(
155 gaia_urls
->oauth1_login_url(), HandleOAuthLogin
);
157 // Handles /ServiceLoginAuth GAIA call.
158 REGISTER_RESPONSE_HANDLER(
159 gaia_urls
->service_login_auth_url(), HandleServiceLoginAuth
);
161 // Handles /SSO GAIA call (not GAIA, made up for SAML tests).
162 REGISTER_PATH_RESPONSE_HANDLER("/SSO", HandleSSO
);
164 // Handles /o/oauth2/token GAIA call.
165 REGISTER_RESPONSE_HANDLER(
166 gaia_urls
->oauth2_token_url(), HandleAuthToken
);
168 // Handles /oauth2/v2/tokeninfo GAIA call.
169 REGISTER_RESPONSE_HANDLER(
170 gaia_urls
->oauth2_token_info_url(), HandleTokenInfo
);
172 // Handles /oauth2/v2/IssueToken GAIA call.
173 REGISTER_RESPONSE_HANDLER(
174 gaia_urls
->oauth2_issue_token_url(), HandleIssueToken
);
176 // Handles /ListAccounts GAIA call.
177 REGISTER_RESPONSE_HANDLER(
178 gaia_urls
->list_accounts_url(), HandleListAccounts
);
181 scoped_ptr
<HttpResponse
> FakeGaia::HandleRequest(const HttpRequest
& request
) {
182 // The scheme and host of the URL is actually not important but required to
183 // get a valid GURL in order to parse |request.relative_url|.
184 GURL request_url
= GURL("http://localhost").Resolve(request
.relative_url
);
185 std::string request_path
= request_url
.path();
186 scoped_ptr
<BasicHttpResponse
> http_response(new BasicHttpResponse());
187 RequestHandlerMap::iterator iter
= request_handlers_
.find(request_path
);
188 if (iter
!= request_handlers_
.end()) {
189 LOG(WARNING
) << "Serving request " << request_path
;
190 iter
->second
.Run(request
, http_response
.get());
192 LOG(ERROR
) << "Unhandled request " << request_path
;
193 return scoped_ptr
<HttpResponse
>(); // Request not understood.
196 return http_response
.PassAs
<HttpResponse
>();
199 void FakeGaia::IssueOAuthToken(const std::string
& auth_token
,
200 const AccessTokenInfo
& token_info
) {
201 access_token_info_map_
.insert(std::make_pair(auth_token
, token_info
));
204 void FakeGaia::RegisterSamlUser(const std::string
& account_id
,
205 const GURL
& saml_idp
) {
206 saml_account_idp_map_
[account_id
] = saml_idp
;
210 bool FakeGaia::GetQueryParameter(const std::string
& query
,
211 const std::string
& key
,
212 std::string
* value
) {
213 // Name and scheme actually don't matter, but are required to get a valid URL
215 GURL
query_url("http://localhost?" + query
);
216 return net::GetValueForKeyInQuery(query_url
, key
, value
);
219 void FakeGaia::HandleMergeSession(const HttpRequest
& request
,
220 BasicHttpResponse
* http_response
) {
221 http_response
->set_code(net::HTTP_UNAUTHORIZED
);
222 if (merge_session_params_
.session_sid_cookie
.empty() ||
223 merge_session_params_
.session_lsid_cookie
.empty()) {
224 http_response
->set_code(net::HTTP_BAD_REQUEST
);
228 std::string uber_token
;
229 if (!GetQueryParameter(request
.content
, "uberauth", &uber_token
) ||
230 uber_token
!= merge_session_params_
.gaia_uber_token
) {
231 LOG(ERROR
) << "Missing or invalid 'uberauth' param in /MergeSession call";
235 std::string continue_url
;
236 if (!GetQueryParameter(request
.content
, "continue", &continue_url
)) {
237 LOG(ERROR
) << "Missing or invalid 'continue' param in /MergeSession call";
242 if (!GetQueryParameter(request
.content
, "source", &source
)) {
243 LOG(ERROR
) << "Missing or invalid 'source' param in /MergeSession call";
247 SetCookies(http_response
,
248 merge_session_params_
.session_sid_cookie
,
249 merge_session_params_
.session_lsid_cookie
);
250 // TODO(zelidrag): Not used now.
251 http_response
->set_content("OK");
252 http_response
->set_code(net::HTTP_OK
);
255 void FakeGaia::HandleProgramaticAuth(
256 const HttpRequest
& request
,
257 BasicHttpResponse
* http_response
) {
258 http_response
->set_code(net::HTTP_UNAUTHORIZED
);
259 if (merge_session_params_
.auth_code
.empty()) {
260 http_response
->set_code(net::HTTP_BAD_REQUEST
);
264 GaiaUrls
* gaia_urls
= GaiaUrls::GetInstance();
266 if (!GetQueryParameter(request
.content
, "scope", &scope
) ||
267 GaiaConstants::kOAuth1LoginScope
!= scope
) {
271 CookieMap cookies
= GetRequestCookies(request
);
272 CookieMap::const_iterator sid_iter
= cookies
.find("SID");
273 if (sid_iter
== cookies
.end() ||
274 sid_iter
->second
!= merge_session_params_
.auth_sid_cookie
) {
275 LOG(ERROR
) << "/o/oauth2/programmatic_auth missing SID cookie";
278 CookieMap::const_iterator lsid_iter
= cookies
.find("LSID");
279 if (lsid_iter
== cookies
.end() ||
280 lsid_iter
->second
!= merge_session_params_
.auth_lsid_cookie
) {
281 LOG(ERROR
) << "/o/oauth2/programmatic_auth missing LSID cookie";
285 std::string client_id
;
286 if (!GetQueryParameter(request
.content
, "client_id", &client_id
) ||
287 gaia_urls
->oauth2_chrome_client_id() != client_id
) {
291 http_response
->AddCustomHeader(
294 "oauth_code=%s; Path=/o/GetOAuth2Token; Secure; HttpOnly;",
295 merge_session_params_
.auth_code
.c_str()));
296 http_response
->set_code(net::HTTP_OK
);
297 http_response
->set_content_type("text/html");
300 void FakeGaia::FormatJSONResponse(const base::DictionaryValue
& response_dict
,
301 BasicHttpResponse
* http_response
) {
302 std::string response_json
;
303 base::JSONWriter::Write(&response_dict
, &response_json
);
304 http_response
->set_content(response_json
);
305 http_response
->set_code(net::HTTP_OK
);
308 const FakeGaia::AccessTokenInfo
* FakeGaia::FindAccessTokenInfo(
309 const std::string
& auth_token
,
310 const std::string
& client_id
,
311 const std::string
& scope_string
) const {
312 if (auth_token
.empty() || client_id
.empty())
315 std::vector
<std::string
> scope_list
;
316 base::SplitString(scope_string
, ' ', &scope_list
);
317 ScopeSet
scopes(scope_list
.begin(), scope_list
.end());
319 for (AccessTokenInfoMap::const_iterator
entry(
320 access_token_info_map_
.lower_bound(auth_token
));
321 entry
!= access_token_info_map_
.upper_bound(auth_token
);
323 if (entry
->second
.audience
== client_id
&&
324 (scope_string
.empty() || entry
->second
.scopes
== scopes
)) {
325 return &(entry
->second
);
332 void FakeGaia::HandleServiceLogin(const HttpRequest
& request
,
333 BasicHttpResponse
* http_response
) {
334 http_response
->set_code(net::HTTP_OK
);
335 http_response
->set_content(service_login_response_
);
336 http_response
->set_content_type("text/html");
339 void FakeGaia::HandleOAuthLogin(const HttpRequest
& request
,
340 BasicHttpResponse
* http_response
) {
341 http_response
->set_code(net::HTTP_UNAUTHORIZED
);
342 if (merge_session_params_
.gaia_uber_token
.empty()) {
343 http_response
->set_code(net::HTTP_FORBIDDEN
);
347 std::string access_token
;
348 if (!GetAccessToken(request
, kAuthHeaderOAuth
, &access_token
)) {
349 LOG(ERROR
) << "/OAuthLogin missing access token in the header";
353 GURL request_url
= GURL("http://localhost").Resolve(request
.relative_url
);
354 std::string request_query
= request_url
.query();
357 if (!GetQueryParameter(request_query
, "source", &source
)) {
358 LOG(ERROR
) << "Missing 'source' param in /OAuthLogin call";
362 std::string issue_uberauth
;
363 if (GetQueryParameter(request_query
, "issueuberauth", &issue_uberauth
) &&
364 issue_uberauth
== "1") {
365 http_response
->set_content(merge_session_params_
.gaia_uber_token
);
366 http_response
->set_code(net::HTTP_OK
);
367 // Issue GAIA uber token.
369 LOG(FATAL
) << "/OAuthLogin for SID/LSID is not supported";
373 void FakeGaia::HandleServiceLoginAuth(const HttpRequest
& request
,
374 BasicHttpResponse
* http_response
) {
375 std::string continue_url
=
376 GaiaUrls::GetInstance()->service_login_url().spec();
377 GetQueryParameter(request
.content
, "continue", &continue_url
);
379 std::string redirect_url
= continue_url
;
382 if (GetQueryParameter(request
.content
, "Email", &email
) &&
383 saml_account_idp_map_
.find(email
) != saml_account_idp_map_
.end()) {
384 GURL
url(saml_account_idp_map_
[email
]);
385 url
= net::AppendQueryParameter(url
, "SAMLRequest", "fake_request");
386 url
= net::AppendQueryParameter(url
, "RelayState", continue_url
);
387 redirect_url
= url
.spec();
388 http_response
->AddCustomHeader("Google-Accounts-SAML", "Start");
389 } else if (!merge_session_params_
.auth_sid_cookie
.empty() &&
390 !merge_session_params_
.auth_lsid_cookie
.empty()) {
391 SetCookies(http_response
,
392 merge_session_params_
.auth_sid_cookie
,
393 merge_session_params_
.auth_lsid_cookie
);
396 http_response
->set_code(net::HTTP_TEMPORARY_REDIRECT
);
397 http_response
->AddCustomHeader("Location", redirect_url
);
400 void FakeGaia::HandleSSO(const HttpRequest
& request
,
401 BasicHttpResponse
* http_response
) {
402 if (!merge_session_params_
.auth_sid_cookie
.empty() &&
403 !merge_session_params_
.auth_lsid_cookie
.empty()) {
404 SetCookies(http_response
,
405 merge_session_params_
.auth_sid_cookie
,
406 merge_session_params_
.auth_lsid_cookie
);
408 std::string relay_state
;
409 GetQueryParameter(request
.content
, "RelayState", &relay_state
);
410 std::string redirect_url
= relay_state
;
411 http_response
->set_code(net::HTTP_TEMPORARY_REDIRECT
);
412 http_response
->AddCustomHeader("Location", redirect_url
);
413 http_response
->AddCustomHeader("Google-Accounts-SAML", "End");
416 void FakeGaia::HandleAuthToken(const HttpRequest
& request
,
417 BasicHttpResponse
* http_response
) {
419 GetQueryParameter(request
.content
, "scope", &scope
);
421 std::string grant_type
;
422 if (!GetQueryParameter(request
.content
, "grant_type", &grant_type
)) {
423 http_response
->set_code(net::HTTP_BAD_REQUEST
);
424 LOG(ERROR
) << "No 'grant_type' param in /o/oauth2/token";
428 if (grant_type
== "authorization_code") {
429 std::string auth_code
;
430 if (!GetQueryParameter(request
.content
, "code", &auth_code
) ||
431 auth_code
!= merge_session_params_
.auth_code
) {
432 http_response
->set_code(net::HTTP_BAD_REQUEST
);
433 LOG(ERROR
) << "No 'code' param in /o/oauth2/token";
437 if (GaiaConstants::kOAuth1LoginScope
!= scope
) {
438 http_response
->set_code(net::HTTP_BAD_REQUEST
);
439 LOG(ERROR
) << "Invalid scope for /o/oauth2/token - " << scope
;
443 base::DictionaryValue response_dict
;
444 response_dict
.SetString("refresh_token",
445 merge_session_params_
.refresh_token
);
446 response_dict
.SetString("access_token",
447 merge_session_params_
.access_token
);
448 response_dict
.SetInteger("expires_in", 3600);
449 FormatJSONResponse(response_dict
, http_response
);
453 std::string refresh_token
;
454 std::string client_id
;
455 if (GetQueryParameter(request
.content
, "refresh_token", &refresh_token
) &&
456 GetQueryParameter(request
.content
, "client_id", &client_id
)) {
457 const AccessTokenInfo
* token_info
=
458 FindAccessTokenInfo(refresh_token
, client_id
, scope
);
460 base::DictionaryValue response_dict
;
461 response_dict
.SetString("access_token", token_info
->token
);
462 response_dict
.SetInteger("expires_in", 3600);
463 FormatJSONResponse(response_dict
, http_response
);
468 LOG(ERROR
) << "Bad request for /o/oauth2/token - "
469 << "refresh_token = " << refresh_token
470 << ", scope = " << scope
471 << ", client_id = " << client_id
;
472 http_response
->set_code(net::HTTP_BAD_REQUEST
);
475 void FakeGaia::HandleTokenInfo(const HttpRequest
& request
,
476 BasicHttpResponse
* http_response
) {
477 const AccessTokenInfo
* token_info
= NULL
;
478 std::string access_token
;
479 if (GetQueryParameter(request
.content
, "access_token", &access_token
)) {
480 for (AccessTokenInfoMap::const_iterator
entry(
481 access_token_info_map_
.begin());
482 entry
!= access_token_info_map_
.end();
484 if (entry
->second
.token
== access_token
) {
485 token_info
= &(entry
->second
);
492 base::DictionaryValue response_dict
;
493 response_dict
.SetString("issued_to", token_info
->issued_to
);
494 response_dict
.SetString("audience", token_info
->audience
);
495 response_dict
.SetString("user_id", token_info
->user_id
);
496 std::vector
<std::string
> scope_vector(token_info
->scopes
.begin(),
497 token_info
->scopes
.end());
498 response_dict
.SetString("scope", JoinString(scope_vector
, " "));
499 response_dict
.SetInteger("expires_in", token_info
->expires_in
);
500 response_dict
.SetString("email", token_info
->email
);
501 FormatJSONResponse(response_dict
, http_response
);
503 http_response
->set_code(net::HTTP_BAD_REQUEST
);
507 void FakeGaia::HandleIssueToken(const HttpRequest
& request
,
508 BasicHttpResponse
* http_response
) {
509 std::string access_token
;
511 std::string client_id
;
512 if (GetAccessToken(request
, kAuthHeaderBearer
, &access_token
) &&
513 GetQueryParameter(request
.content
, "scope", &scope
) &&
514 GetQueryParameter(request
.content
, "client_id", &client_id
)) {
515 const AccessTokenInfo
* token_info
=
516 FindAccessTokenInfo(access_token
, client_id
, scope
);
518 base::DictionaryValue response_dict
;
519 response_dict
.SetString("issueAdvice", "auto");
520 response_dict
.SetString("expiresIn",
521 base::IntToString(token_info
->expires_in
));
522 response_dict
.SetString("token", token_info
->token
);
523 FormatJSONResponse(response_dict
, http_response
);
527 http_response
->set_code(net::HTTP_BAD_REQUEST
);
530 void FakeGaia::HandleListAccounts(const HttpRequest
& request
,
531 BasicHttpResponse
* http_response
) {
532 http_response
->set_content(base::StringPrintf(
533 kListAccountsResponseFormat
, merge_session_params_
.email
.c_str()));
534 http_response
->set_code(net::HTTP_OK
);