fake_gaia.cc revision 1320f92c476a1ad9d19dba2a48c72b75566198e9
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