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