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 40 using namespace net::test_server; 41 42 namespace { 43 44 const base::FilePath::CharType kServiceLogin[] = 45 FILE_PATH_LITERAL("google_apis/test/service_login.html"); 46 47 // OAuth2 Authentication header value prefix. 48 const char kAuthHeaderBearer[] = "Bearer "; 49 const char kAuthHeaderOAuth[] = "OAuth "; 50 51 const char kListAccountsResponseFormat[] = 52 "[\"gaia.l.a.r\",[[\"gaia.l.a\",1,\"\",\"%s\",\"\",1,1,0]]]"; 53 54 typedef std::map<std::string, std::string> CookieMap; 55 56 // Parses cookie name-value map our of |request|. 57 CookieMap GetRequestCookies(const HttpRequest& request) { 58 CookieMap result; 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(); 67 ++cookie_line) { 68 std::vector<std::string> name_value; 69 base::SplitString(*cookie_line, '=', &name_value); 70 if (name_value.size() != 2) 71 continue; 72 73 std::string value = name_value[1]; 74 if (value.size() && value[value.size() - 1] == ';') 75 value = value.substr(0, value.size() -1); 76 77 result.insert(std::make_pair(name_value[0], value)); 78 } 79 } 80 return result; 81 } 82 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)); 93 return true; 94 } 95 } 96 97 return false; 98 } 99 100 void SetCookies(BasicHttpResponse* http_response, 101 const std::string& sid_cookie, 102 const std::string& lsid_cookie) { 103 http_response->AddCustomHeader( 104 "Set-Cookie", 105 base::StringPrintf("SID=%s; Path=/; HttpOnly;", sid_cookie.c_str())); 106 http_response->AddCustomHeader( 107 "Set-Cookie", 108 base::StringPrintf("LSID=%s; Path=/; HttpOnly;", lsid_cookie.c_str())); 109 } 110 111 } // namespace 112 113 FakeGaia::AccessTokenInfo::AccessTokenInfo() 114 : expires_in(3600) {} 115 116 FakeGaia::AccessTokenInfo::~AccessTokenInfo() {} 117 118 FakeGaia::MergeSessionParams::MergeSessionParams() { 119 } 120 121 FakeGaia::MergeSessionParams::~MergeSessionParams() { 122 } 123 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_)); 130 } 131 132 FakeGaia::~FakeGaia() {} 133 134 void FakeGaia::SetMergeSessionParams( 135 const MergeSessionParams& params) { 136 merge_session_params_ = params; 137 } 138 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); 144 145 // Handles /o/oauth2/programmatic_auth GAIA call. 146 REGISTER_RESPONSE_HANDLER( 147 gaia_urls->client_login_to_oauth2_url(), HandleProgramaticAuth); 148 149 // Handles /ServiceLogin GAIA call. 150 REGISTER_RESPONSE_HANDLER( 151 gaia_urls->service_login_url(), HandleServiceLogin); 152 153 // Handles /OAuthLogin GAIA call. 154 REGISTER_RESPONSE_HANDLER( 155 gaia_urls->oauth1_login_url(), HandleOAuthLogin); 156 157 // Handles /ServiceLoginAuth GAIA call. 158 REGISTER_RESPONSE_HANDLER( 159 gaia_urls->service_login_auth_url(), HandleServiceLoginAuth); 160 161 // Handles /SSO GAIA call (not GAIA, made up for SAML tests). 162 REGISTER_PATH_RESPONSE_HANDLER("/SSO", HandleSSO); 163 164 // Handles /o/oauth2/token GAIA call. 165 REGISTER_RESPONSE_HANDLER( 166 gaia_urls->oauth2_token_url(), HandleAuthToken); 167 168 // Handles /oauth2/v2/tokeninfo GAIA call. 169 REGISTER_RESPONSE_HANDLER( 170 gaia_urls->oauth2_token_info_url(), HandleTokenInfo); 171 172 // Handles /oauth2/v2/IssueToken GAIA call. 173 REGISTER_RESPONSE_HANDLER( 174 gaia_urls->oauth2_issue_token_url(), HandleIssueToken); 175 176 // Handles /ListAccounts GAIA call. 177 REGISTER_RESPONSE_HANDLER( 178 gaia_urls->list_accounts_url(), HandleListAccounts); 179 } 180 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()); 191 } else { 192 LOG(ERROR) << "Unhandled request " << request_path; 193 return scoped_ptr<HttpResponse>(); // Request not understood. 194 } 195 196 return http_response.PassAs<HttpResponse>(); 197 } 198 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)); 202 } 203 204 void FakeGaia::RegisterSamlUser(const std::string& account_id, 205 const GURL& saml_idp) { 206 saml_account_idp_map_[account_id] = saml_idp; 207 } 208 209 // static 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 214 // for parsing. 215 GURL query_url("http://localhost?" + query); 216 return net::GetValueForKeyInQuery(query_url, key, value); 217 } 218 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); 225 return; 226 } 227 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"; 232 return; 233 } 234 235 std::string continue_url; 236 if (!GetQueryParameter(request.content, "continue", &continue_url)) { 237 LOG(ERROR) << "Missing or invalid 'continue' param in /MergeSession call"; 238 return; 239 } 240 241 std::string source; 242 if (!GetQueryParameter(request.content, "source", &source)) { 243 LOG(ERROR) << "Missing or invalid 'source' param in /MergeSession call"; 244 return; 245 } 246 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); 253 } 254 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); 261 return; 262 } 263 264 GaiaUrls* gaia_urls = GaiaUrls::GetInstance(); 265 std::string scope; 266 if (!GetQueryParameter(request.content, "scope", &scope) || 267 GaiaConstants::kOAuth1LoginScope != scope) { 268 return; 269 } 270 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"; 276 return; 277 } 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"; 282 return; 283 } 284 285 std::string client_id; 286 if (!GetQueryParameter(request.content, "client_id", &client_id) || 287 gaia_urls->oauth2_chrome_client_id() != client_id) { 288 return; 289 } 290 291 http_response->AddCustomHeader( 292 "Set-Cookie", 293 base::StringPrintf( 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"); 298 } 299 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); 306 } 307 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()) 313 return NULL; 314 315 std::vector<std::string> scope_list; 316 base::SplitString(scope_string, ' ', &scope_list); 317 ScopeSet scopes(scope_list.begin(), scope_list.end()); 318 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); 322 ++entry) { 323 if (entry->second.audience == client_id && 324 (scope_string.empty() || entry->second.scopes == scopes)) { 325 return &(entry->second); 326 } 327 } 328 329 return NULL; 330 } 331 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"); 337 } 338 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); 344 return; 345 } 346 347 std::string access_token; 348 if (!GetAccessToken(request, kAuthHeaderOAuth, &access_token)) { 349 LOG(ERROR) << "/OAuthLogin missing access token in the header"; 350 return; 351 } 352 353 GURL request_url = GURL("http://localhost").Resolve(request.relative_url); 354 std::string request_query = request_url.query(); 355 356 std::string source; 357 if (!GetQueryParameter(request_query, "source", &source)) { 358 LOG(ERROR) << "Missing 'source' param in /OAuthLogin call"; 359 return; 360 } 361 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. 368 } else { 369 LOG(FATAL) << "/OAuthLogin for SID/LSID is not supported"; 370 } 371 } 372 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); 378 379 std::string redirect_url = continue_url; 380 381 std::string email; 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 } else if (!merge_session_params_.auth_sid_cookie.empty() && 389 !merge_session_params_.auth_lsid_cookie.empty()) { 390 SetCookies(http_response, 391 merge_session_params_.auth_sid_cookie, 392 merge_session_params_.auth_lsid_cookie); 393 } 394 395 http_response->set_code(net::HTTP_TEMPORARY_REDIRECT); 396 http_response->AddCustomHeader("Location", redirect_url); 397 } 398 399 void FakeGaia::HandleSSO(const HttpRequest& request, 400 BasicHttpResponse* http_response) { 401 if (!merge_session_params_.auth_sid_cookie.empty() && 402 !merge_session_params_.auth_lsid_cookie.empty()) { 403 SetCookies(http_response, 404 merge_session_params_.auth_sid_cookie, 405 merge_session_params_.auth_lsid_cookie); 406 } 407 std::string relay_state; 408 GetQueryParameter(request.content, "RelayState", &relay_state); 409 std::string redirect_url = relay_state; 410 http_response->set_code(net::HTTP_TEMPORARY_REDIRECT); 411 http_response->AddCustomHeader("Location", redirect_url); 412 } 413 414 void FakeGaia::HandleAuthToken(const HttpRequest& request, 415 BasicHttpResponse* http_response) { 416 std::string grant_type; 417 std::string refresh_token; 418 std::string client_id; 419 std::string scope; 420 std::string auth_code; 421 const AccessTokenInfo* token_info = NULL; 422 GetQueryParameter(request.content, "scope", &scope); 423 424 if (!GetQueryParameter(request.content, "grant_type", &grant_type)) { 425 http_response->set_code(net::HTTP_BAD_REQUEST); 426 LOG(ERROR) << "No 'grant_type' param in /o/oauth2/token"; 427 return; 428 } 429 430 if (grant_type == "authorization_code") { 431 if (!GetQueryParameter(request.content, "code", &auth_code) || 432 auth_code != merge_session_params_.auth_code) { 433 http_response->set_code(net::HTTP_BAD_REQUEST); 434 LOG(ERROR) << "No 'code' param in /o/oauth2/token"; 435 return; 436 } 437 438 if (GaiaConstants::kOAuth1LoginScope != scope) { 439 http_response->set_code(net::HTTP_BAD_REQUEST); 440 LOG(ERROR) << "Invalid scope for /o/oauth2/token - " << scope; 441 return; 442 } 443 444 base::DictionaryValue response_dict; 445 response_dict.SetString("refresh_token", 446 merge_session_params_.refresh_token); 447 response_dict.SetString("access_token", 448 merge_session_params_.access_token); 449 response_dict.SetInteger("expires_in", 3600); 450 FormatJSONResponse(response_dict, http_response); 451 } else if (GetQueryParameter(request.content, 452 "refresh_token", 453 &refresh_token) && 454 GetQueryParameter(request.content, 455 "client_id", 456 &client_id) && 457 (token_info = FindAccessTokenInfo(refresh_token, 458 client_id, 459 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); 464 } else { 465 LOG(ERROR) << "Bad request for /o/oauth2/token - " 466 << "refresh_token = " << refresh_token 467 << ", scope = " << scope 468 << ", client_id = " << client_id; 469 http_response->set_code(net::HTTP_BAD_REQUEST); 470 } 471 } 472 473 void FakeGaia::HandleTokenInfo(const HttpRequest& request, 474 BasicHttpResponse* http_response) { 475 const AccessTokenInfo* token_info = NULL; 476 std::string access_token; 477 if (GetQueryParameter(request.content, "access_token", &access_token)) { 478 for (AccessTokenInfoMap::const_iterator entry( 479 access_token_info_map_.begin()); 480 entry != access_token_info_map_.end(); 481 ++entry) { 482 if (entry->second.token == access_token) { 483 token_info = &(entry->second); 484 break; 485 } 486 } 487 } 488 489 if (token_info) { 490 base::DictionaryValue response_dict; 491 response_dict.SetString("issued_to", token_info->issued_to); 492 response_dict.SetString("audience", token_info->audience); 493 response_dict.SetString("user_id", token_info->user_id); 494 std::vector<std::string> scope_vector(token_info->scopes.begin(), 495 token_info->scopes.end()); 496 response_dict.SetString("scope", JoinString(scope_vector, " ")); 497 response_dict.SetInteger("expires_in", token_info->expires_in); 498 response_dict.SetString("email", token_info->email); 499 FormatJSONResponse(response_dict, http_response); 500 } else { 501 http_response->set_code(net::HTTP_BAD_REQUEST); 502 } 503 } 504 505 void FakeGaia::HandleIssueToken(const HttpRequest& request, 506 BasicHttpResponse* http_response) { 507 std::string access_token; 508 std::string scope; 509 std::string client_id; 510 const AccessTokenInfo* token_info = NULL; 511 if (GetAccessToken(request, kAuthHeaderBearer, &access_token) && 512 GetQueryParameter(request.content, "scope", &scope) && 513 GetQueryParameter(request.content, "client_id", &client_id) && 514 (token_info = FindAccessTokenInfo(access_token, client_id, scope))) { 515 base::DictionaryValue response_dict; 516 response_dict.SetString("issueAdvice", "auto"); 517 response_dict.SetString("expiresIn", 518 base::IntToString(token_info->expires_in)); 519 response_dict.SetString("token", token_info->token); 520 FormatJSONResponse(response_dict, http_response); 521 } else { 522 http_response->set_code(net::HTTP_BAD_REQUEST); 523 } 524 } 525 526 void FakeGaia::HandleListAccounts(const HttpRequest& request, 527 BasicHttpResponse* http_response) { 528 http_response->set_content(base::StringPrintf( 529 kListAccountsResponseFormat, merge_session_params_.email.c_str())); 530 http_response->set_code(net::HTTP_OK); 531 } 532