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. 4 5#include "google_apis/gaia/fake_gaia.h" 6 7#include <vector> 8 9#include "base/base_paths.h" 10#include "base/bind.h" 11#include "base/bind_helpers.h" 12#include "base/files/file_path.h" 13#include "base/files/file_util.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" 31 32#define REGISTER_RESPONSE_HANDLER(url, method) \ 33 request_handlers_.insert(std::make_pair( \ 34 url.path(), base::Bind(&FakeGaia::method, base::Unretained(this)))) 35 36#define REGISTER_PATH_RESPONSE_HANDLER(path, method) \ 37 request_handlers_.insert(std::make_pair( \ 38 path, base::Bind(&FakeGaia::method, base::Unretained(this)))) 39 40using namespace net::test_server; 41 42namespace { 43 44const char kTestAuthCode[] = "fake-auth-code"; 45const char kTestGaiaUberToken[] = "fake-uber-token"; 46const char kTestAuthLoginAccessToken[] = "fake-access-token"; 47const char kTestRefreshToken[] = "fake-refresh-token"; 48const char kTestSessionSIDCookie[] = "fake-session-SID-cookie"; 49const char kTestSessionLSIDCookie[] = "fake-session-LSID-cookie"; 50const char kTestOAuthLoginSID[] = "fake-oauth-SID-cookie"; 51const char kTestOAuthLoginLSID[] = "fake-oauth-LSID-cookie"; 52const char kTestOAuthLoginAuthCode[] = "fake-oauth-auth-code"; 53 54const base::FilePath::CharType kServiceLogin[] = 55 FILE_PATH_LITERAL("google_apis/test/service_login.html"); 56 57// OAuth2 Authentication header value prefix. 58const char kAuthHeaderBearer[] = "Bearer "; 59const char kAuthHeaderOAuth[] = "OAuth "; 60 61const char kListAccountsResponseFormat[] = 62 "[\"gaia.l.a.r\",[[\"gaia.l.a\",1,\"\",\"%s\",\"\",1,1,0]]]"; 63 64typedef std::map<std::string, std::string> CookieMap; 65 66// Parses cookie name-value map our of |request|. 67CookieMap GetRequestCookies(const HttpRequest& request) { 68 CookieMap result; 69 std::map<std::string, std::string>::const_iterator iter = 70 request.headers.find("Cookie"); 71 if (iter != request.headers.end()) { 72 std::vector<std::string> cookie_nv_pairs; 73 base::SplitString(iter->second, ' ', &cookie_nv_pairs); 74 for(std::vector<std::string>::const_iterator cookie_line = 75 cookie_nv_pairs.begin(); 76 cookie_line != cookie_nv_pairs.end(); 77 ++cookie_line) { 78 std::vector<std::string> name_value; 79 base::SplitString(*cookie_line, '=', &name_value); 80 if (name_value.size() != 2) 81 continue; 82 83 std::string value = name_value[1]; 84 if (value.size() && value[value.size() - 1] == ';') 85 value = value.substr(0, value.size() -1); 86 87 result.insert(std::make_pair(name_value[0], value)); 88 } 89 } 90 return result; 91} 92 93// Extracts the |access_token| from authorization header of |request|. 94bool GetAccessToken(const HttpRequest& request, 95 const char* auth_token_prefix, 96 std::string* access_token) { 97 std::map<std::string, std::string>::const_iterator auth_header_entry = 98 request.headers.find("Authorization"); 99 if (auth_header_entry != request.headers.end()) { 100 if (StartsWithASCII(auth_header_entry->second, auth_token_prefix, true)) { 101 *access_token = auth_header_entry->second.substr( 102 strlen(auth_token_prefix)); 103 return true; 104 } 105 } 106 107 return false; 108} 109 110void SetCookies(BasicHttpResponse* http_response, 111 const std::string& sid_cookie, 112 const std::string& lsid_cookie) { 113 http_response->AddCustomHeader( 114 "Set-Cookie", 115 base::StringPrintf("SID=%s; Path=/; HttpOnly", sid_cookie.c_str())); 116 http_response->AddCustomHeader( 117 "Set-Cookie", 118 base::StringPrintf("LSID=%s; Path=/; HttpOnly", lsid_cookie.c_str())); 119} 120 121} // namespace 122 123FakeGaia::AccessTokenInfo::AccessTokenInfo() 124 : expires_in(3600) {} 125 126FakeGaia::AccessTokenInfo::~AccessTokenInfo() {} 127 128FakeGaia::MergeSessionParams::MergeSessionParams() { 129} 130 131FakeGaia::MergeSessionParams::~MergeSessionParams() { 132} 133 134FakeGaia::FakeGaia() { 135 base::FilePath source_root_dir; 136 PathService::Get(base::DIR_SOURCE_ROOT, &source_root_dir); 137 CHECK(base::ReadFileToString( 138 source_root_dir.Append(base::FilePath(kServiceLogin)), 139 &service_login_response_)); 140} 141 142FakeGaia::~FakeGaia() {} 143 144void FakeGaia::SetFakeMergeSessionParams( 145 const std::string& email, 146 const std::string& auth_sid_cookie, 147 const std::string& auth_lsid_cookie) { 148 FakeGaia::MergeSessionParams params; 149 params.auth_sid_cookie = auth_sid_cookie; 150 params.auth_lsid_cookie = auth_lsid_cookie; 151 params.auth_code = kTestAuthCode; 152 params.refresh_token = kTestRefreshToken; 153 params.access_token = kTestAuthLoginAccessToken; 154 params.gaia_uber_token = kTestGaiaUberToken; 155 params.session_sid_cookie = kTestSessionSIDCookie; 156 params.session_lsid_cookie = kTestSessionLSIDCookie; 157 params.email = email; 158 SetMergeSessionParams(params); 159} 160 161void FakeGaia::SetMergeSessionParams( 162 const MergeSessionParams& params) { 163 merge_session_params_ = params; 164} 165 166void FakeGaia::Initialize() { 167 GaiaUrls* gaia_urls = GaiaUrls::GetInstance(); 168 // Handles /MergeSession GAIA call. 169 REGISTER_RESPONSE_HANDLER( 170 gaia_urls->merge_session_url(), HandleMergeSession); 171 172 // Handles /o/oauth2/programmatic_auth GAIA call. 173 REGISTER_RESPONSE_HANDLER( 174 gaia_urls->client_login_to_oauth2_url(), HandleProgramaticAuth); 175 176 // Handles /ServiceLogin GAIA call. 177 REGISTER_RESPONSE_HANDLER( 178 gaia_urls->service_login_url(), HandleServiceLogin); 179 180 // Handles /OAuthLogin GAIA call. 181 REGISTER_RESPONSE_HANDLER( 182 gaia_urls->oauth1_login_url(), HandleOAuthLogin); 183 184 // Handles /ServiceLoginAuth GAIA call. 185 REGISTER_RESPONSE_HANDLER( 186 gaia_urls->service_login_auth_url(), HandleServiceLoginAuth); 187 188 // Handles /SSO GAIA call (not GAIA, made up for SAML tests). 189 REGISTER_PATH_RESPONSE_HANDLER("/SSO", HandleSSO); 190 191 // Handles /o/oauth2/token GAIA call. 192 REGISTER_RESPONSE_HANDLER( 193 gaia_urls->oauth2_token_url(), HandleAuthToken); 194 195 // Handles /oauth2/v2/tokeninfo GAIA call. 196 REGISTER_RESPONSE_HANDLER( 197 gaia_urls->oauth2_token_info_url(), HandleTokenInfo); 198 199 // Handles /oauth2/v2/IssueToken GAIA call. 200 REGISTER_RESPONSE_HANDLER( 201 gaia_urls->oauth2_issue_token_url(), HandleIssueToken); 202 203 // Handles /ListAccounts GAIA call. 204 REGISTER_RESPONSE_HANDLER( 205 gaia_urls->list_accounts_url(), HandleListAccounts); 206 207 // Handles /GetUserInfo GAIA call. 208 REGISTER_RESPONSE_HANDLER( 209 gaia_urls->get_user_info_url(), HandleGetUserInfo); 210} 211 212scoped_ptr<HttpResponse> FakeGaia::HandleRequest(const HttpRequest& request) { 213 // The scheme and host of the URL is actually not important but required to 214 // get a valid GURL in order to parse |request.relative_url|. 215 GURL request_url = GURL("http://localhost").Resolve(request.relative_url); 216 std::string request_path = request_url.path(); 217 scoped_ptr<BasicHttpResponse> http_response(new BasicHttpResponse()); 218 RequestHandlerMap::iterator iter = request_handlers_.find(request_path); 219 if (iter != request_handlers_.end()) { 220 LOG(WARNING) << "Serving request " << request_path; 221 iter->second.Run(request, http_response.get()); 222 } else { 223 LOG(ERROR) << "Unhandled request " << request_path; 224 return scoped_ptr<HttpResponse>(); // Request not understood. 225 } 226 227 return http_response.PassAs<HttpResponse>(); 228} 229 230void FakeGaia::IssueOAuthToken(const std::string& auth_token, 231 const AccessTokenInfo& token_info) { 232 access_token_info_map_.insert(std::make_pair(auth_token, token_info)); 233} 234 235void FakeGaia::RegisterSamlUser(const std::string& account_id, 236 const GURL& saml_idp) { 237 saml_account_idp_map_[account_id] = saml_idp; 238} 239 240// static 241bool FakeGaia::GetQueryParameter(const std::string& query, 242 const std::string& key, 243 std::string* value) { 244 // Name and scheme actually don't matter, but are required to get a valid URL 245 // for parsing. 246 GURL query_url("http://localhost?" + query); 247 return net::GetValueForKeyInQuery(query_url, key, value); 248} 249 250void FakeGaia::HandleMergeSession(const HttpRequest& request, 251 BasicHttpResponse* http_response) { 252 http_response->set_code(net::HTTP_UNAUTHORIZED); 253 if (merge_session_params_.session_sid_cookie.empty() || 254 merge_session_params_.session_lsid_cookie.empty()) { 255 http_response->set_code(net::HTTP_BAD_REQUEST); 256 return; 257 } 258 259 std::string uber_token; 260 if (!GetQueryParameter(request.content, "uberauth", &uber_token) || 261 uber_token != merge_session_params_.gaia_uber_token) { 262 LOG(ERROR) << "Missing or invalid 'uberauth' param in /MergeSession call"; 263 return; 264 } 265 266 std::string continue_url; 267 if (!GetQueryParameter(request.content, "continue", &continue_url)) { 268 LOG(ERROR) << "Missing or invalid 'continue' param in /MergeSession call"; 269 return; 270 } 271 272 std::string source; 273 if (!GetQueryParameter(request.content, "source", &source)) { 274 LOG(ERROR) << "Missing or invalid 'source' param in /MergeSession call"; 275 return; 276 } 277 278 SetCookies(http_response, 279 merge_session_params_.session_sid_cookie, 280 merge_session_params_.session_lsid_cookie); 281 // TODO(zelidrag): Not used now. 282 http_response->set_content("OK"); 283 http_response->set_code(net::HTTP_OK); 284} 285 286void FakeGaia::HandleProgramaticAuth( 287 const HttpRequest& request, 288 BasicHttpResponse* http_response) { 289 http_response->set_code(net::HTTP_UNAUTHORIZED); 290 if (merge_session_params_.auth_code.empty()) { 291 http_response->set_code(net::HTTP_BAD_REQUEST); 292 return; 293 } 294 295 GaiaUrls* gaia_urls = GaiaUrls::GetInstance(); 296 std::string scope; 297 if (!GetQueryParameter(request.content, "scope", &scope) || 298 GaiaConstants::kOAuth1LoginScope != scope) { 299 return; 300 } 301 302 CookieMap cookies = GetRequestCookies(request); 303 CookieMap::const_iterator sid_iter = cookies.find("SID"); 304 if (sid_iter == cookies.end() || 305 sid_iter->second != merge_session_params_.auth_sid_cookie) { 306 LOG(ERROR) << "/o/oauth2/programmatic_auth missing SID cookie"; 307 return; 308 } 309 CookieMap::const_iterator lsid_iter = cookies.find("LSID"); 310 if (lsid_iter == cookies.end() || 311 lsid_iter->second != merge_session_params_.auth_lsid_cookie) { 312 LOG(ERROR) << "/o/oauth2/programmatic_auth missing LSID cookie"; 313 return; 314 } 315 316 std::string client_id; 317 if (!GetQueryParameter(request.content, "client_id", &client_id) || 318 gaia_urls->oauth2_chrome_client_id() != client_id) { 319 return; 320 } 321 322 http_response->AddCustomHeader( 323 "Set-Cookie", 324 base::StringPrintf( 325 "oauth_code=%s; Path=/o/GetOAuth2Token; Secure; HttpOnly;", 326 merge_session_params_.auth_code.c_str())); 327 http_response->set_code(net::HTTP_OK); 328 http_response->set_content_type("text/html"); 329} 330 331void FakeGaia::FormatJSONResponse(const base::DictionaryValue& response_dict, 332 BasicHttpResponse* http_response) { 333 std::string response_json; 334 base::JSONWriter::Write(&response_dict, &response_json); 335 http_response->set_content(response_json); 336 http_response->set_code(net::HTTP_OK); 337} 338 339const FakeGaia::AccessTokenInfo* FakeGaia::FindAccessTokenInfo( 340 const std::string& auth_token, 341 const std::string& client_id, 342 const std::string& scope_string) const { 343 if (auth_token.empty() || client_id.empty()) 344 return NULL; 345 346 std::vector<std::string> scope_list; 347 base::SplitString(scope_string, ' ', &scope_list); 348 ScopeSet scopes(scope_list.begin(), scope_list.end()); 349 350 for (AccessTokenInfoMap::const_iterator entry( 351 access_token_info_map_.lower_bound(auth_token)); 352 entry != access_token_info_map_.upper_bound(auth_token); 353 ++entry) { 354 if (entry->second.audience == client_id && 355 (scope_string.empty() || entry->second.scopes == scopes)) { 356 return &(entry->second); 357 } 358 } 359 360 return NULL; 361} 362 363void FakeGaia::HandleServiceLogin(const HttpRequest& request, 364 BasicHttpResponse* http_response) { 365 http_response->set_code(net::HTTP_OK); 366 http_response->set_content(service_login_response_); 367 http_response->set_content_type("text/html"); 368} 369 370void FakeGaia::HandleOAuthLogin(const HttpRequest& request, 371 BasicHttpResponse* http_response) { 372 http_response->set_code(net::HTTP_UNAUTHORIZED); 373 if (merge_session_params_.gaia_uber_token.empty()) { 374 http_response->set_code(net::HTTP_FORBIDDEN); 375 return; 376 } 377 378 std::string access_token; 379 if (!GetAccessToken(request, kAuthHeaderBearer, &access_token) && 380 !GetAccessToken(request, kAuthHeaderOAuth, &access_token)) { 381 LOG(ERROR) << "/OAuthLogin missing access token in the header"; 382 return; 383 } 384 385 GURL request_url = GURL("http://localhost").Resolve(request.relative_url); 386 std::string request_query = request_url.query(); 387 388 std::string source; 389 if (!GetQueryParameter(request_query, "source", &source) && 390 !GetQueryParameter(request.content, "source", &source)) { 391 LOG(ERROR) << "Missing 'source' param in /OAuthLogin call"; 392 return; 393 } 394 395 std::string issue_uberauth; 396 if (GetQueryParameter(request_query, "issueuberauth", &issue_uberauth) && 397 issue_uberauth == "1") { 398 http_response->set_content(merge_session_params_.gaia_uber_token); 399 http_response->set_code(net::HTTP_OK); 400 // Issue GAIA uber token. 401 } else { 402 http_response->set_content(base::StringPrintf( 403 "SID=%s\nLSID=%s\nAuth=%s", 404 kTestOAuthLoginSID, kTestOAuthLoginLSID, kTestOAuthLoginAuthCode)); 405 http_response->set_code(net::HTTP_OK); 406 } 407} 408 409void FakeGaia::HandleServiceLoginAuth(const HttpRequest& request, 410 BasicHttpResponse* http_response) { 411 std::string continue_url = 412 GaiaUrls::GetInstance()->service_login_url().spec(); 413 GetQueryParameter(request.content, "continue", &continue_url); 414 415 std::string redirect_url = continue_url; 416 417 std::string email; 418 if (GetQueryParameter(request.content, "Email", &email) && 419 saml_account_idp_map_.find(email) != saml_account_idp_map_.end()) { 420 GURL url(saml_account_idp_map_[email]); 421 url = net::AppendQueryParameter(url, "SAMLRequest", "fake_request"); 422 url = net::AppendQueryParameter(url, "RelayState", continue_url); 423 redirect_url = url.spec(); 424 http_response->AddCustomHeader("Google-Accounts-SAML", "Start"); 425 } else if (!merge_session_params_.auth_sid_cookie.empty() && 426 !merge_session_params_.auth_lsid_cookie.empty()) { 427 SetCookies(http_response, 428 merge_session_params_.auth_sid_cookie, 429 merge_session_params_.auth_lsid_cookie); 430 } 431 432 http_response->set_code(net::HTTP_TEMPORARY_REDIRECT); 433 http_response->AddCustomHeader("Location", redirect_url); 434 http_response->AddCustomHeader("google-accounts-signin", 435 base::StringPrintf("email=\"%s\", sessionindex=0", email.c_str())); 436} 437 438void FakeGaia::HandleSSO(const HttpRequest& request, 439 BasicHttpResponse* http_response) { 440 if (!merge_session_params_.auth_sid_cookie.empty() && 441 !merge_session_params_.auth_lsid_cookie.empty()) { 442 SetCookies(http_response, 443 merge_session_params_.auth_sid_cookie, 444 merge_session_params_.auth_lsid_cookie); 445 } 446 std::string relay_state; 447 GetQueryParameter(request.content, "RelayState", &relay_state); 448 std::string redirect_url = relay_state; 449 http_response->set_code(net::HTTP_TEMPORARY_REDIRECT); 450 http_response->AddCustomHeader("Location", redirect_url); 451 http_response->AddCustomHeader("Google-Accounts-SAML", "End"); 452} 453 454void FakeGaia::HandleAuthToken(const HttpRequest& request, 455 BasicHttpResponse* http_response) { 456 std::string scope; 457 GetQueryParameter(request.content, "scope", &scope); 458 459 std::string grant_type; 460 if (!GetQueryParameter(request.content, "grant_type", &grant_type)) { 461 http_response->set_code(net::HTTP_BAD_REQUEST); 462 LOG(ERROR) << "No 'grant_type' param in /o/oauth2/token"; 463 return; 464 } 465 466 if (grant_type == "authorization_code") { 467 std::string auth_code; 468 if (!GetQueryParameter(request.content, "code", &auth_code) || 469 auth_code != merge_session_params_.auth_code) { 470 http_response->set_code(net::HTTP_BAD_REQUEST); 471 LOG(ERROR) << "No 'code' param in /o/oauth2/token"; 472 return; 473 } 474 475 if (GaiaConstants::kOAuth1LoginScope != scope) { 476 http_response->set_code(net::HTTP_BAD_REQUEST); 477 LOG(ERROR) << "Invalid scope for /o/oauth2/token - " << scope; 478 return; 479 } 480 481 base::DictionaryValue response_dict; 482 response_dict.SetString("refresh_token", 483 merge_session_params_.refresh_token); 484 response_dict.SetString("access_token", 485 merge_session_params_.access_token); 486 response_dict.SetInteger("expires_in", 3600); 487 FormatJSONResponse(response_dict, http_response); 488 return; 489 } 490 491 std::string refresh_token; 492 std::string client_id; 493 if (GetQueryParameter(request.content, "refresh_token", &refresh_token) && 494 GetQueryParameter(request.content, "client_id", &client_id)) { 495 const AccessTokenInfo* token_info = 496 FindAccessTokenInfo(refresh_token, client_id, scope); 497 if (token_info) { 498 base::DictionaryValue response_dict; 499 response_dict.SetString("access_token", token_info->token); 500 response_dict.SetInteger("expires_in", 3600); 501 FormatJSONResponse(response_dict, http_response); 502 return; 503 } 504 } 505 506 LOG(ERROR) << "Bad request for /o/oauth2/token - " 507 << "refresh_token = " << refresh_token 508 << ", scope = " << scope 509 << ", client_id = " << client_id; 510 http_response->set_code(net::HTTP_BAD_REQUEST); 511} 512 513void FakeGaia::HandleTokenInfo(const HttpRequest& request, 514 BasicHttpResponse* http_response) { 515 const AccessTokenInfo* token_info = NULL; 516 std::string access_token; 517 if (GetQueryParameter(request.content, "access_token", &access_token)) { 518 for (AccessTokenInfoMap::const_iterator entry( 519 access_token_info_map_.begin()); 520 entry != access_token_info_map_.end(); 521 ++entry) { 522 if (entry->second.token == access_token) { 523 token_info = &(entry->second); 524 break; 525 } 526 } 527 } 528 529 if (token_info) { 530 base::DictionaryValue response_dict; 531 response_dict.SetString("issued_to", token_info->issued_to); 532 response_dict.SetString("audience", token_info->audience); 533 response_dict.SetString("user_id", token_info->user_id); 534 std::vector<std::string> scope_vector(token_info->scopes.begin(), 535 token_info->scopes.end()); 536 response_dict.SetString("scope", JoinString(scope_vector, " ")); 537 response_dict.SetInteger("expires_in", token_info->expires_in); 538 response_dict.SetString("email", token_info->email); 539 FormatJSONResponse(response_dict, http_response); 540 } else { 541 http_response->set_code(net::HTTP_BAD_REQUEST); 542 } 543} 544 545void FakeGaia::HandleIssueToken(const HttpRequest& request, 546 BasicHttpResponse* http_response) { 547 std::string access_token; 548 std::string scope; 549 std::string client_id; 550 if (GetAccessToken(request, kAuthHeaderBearer, &access_token) && 551 GetQueryParameter(request.content, "scope", &scope) && 552 GetQueryParameter(request.content, "client_id", &client_id)) { 553 const AccessTokenInfo* token_info = 554 FindAccessTokenInfo(access_token, client_id, scope); 555 if (token_info) { 556 base::DictionaryValue response_dict; 557 response_dict.SetString("issueAdvice", "auto"); 558 response_dict.SetString("expiresIn", 559 base::IntToString(token_info->expires_in)); 560 response_dict.SetString("token", token_info->token); 561 FormatJSONResponse(response_dict, http_response); 562 return; 563 } 564 } 565 http_response->set_code(net::HTTP_BAD_REQUEST); 566} 567 568void FakeGaia::HandleListAccounts(const HttpRequest& request, 569 BasicHttpResponse* http_response) { 570 http_response->set_content(base::StringPrintf( 571 kListAccountsResponseFormat, merge_session_params_.email.c_str())); 572 http_response->set_code(net::HTTP_OK); 573} 574 575void FakeGaia::HandleGetUserInfo(const HttpRequest& request, 576 BasicHttpResponse* http_response) { 577 http_response->set_content(base::StringPrintf( 578 "email=%s\ndisplayEmail=%s", 579 merge_session_params_.email.c_str(), 580 merge_session_params_.email.c_str())); 581 http_response->set_code(net::HTTP_OK); 582} 583