Home | History | Annotate | Download | only in gaia
      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