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