fake_gaia.cc revision 6e8cce623b6e4fe0c9e4af605d675dd9d0338c38
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]]]";
53
54typedef std::map<std::string, std::string> CookieMap;
55
56// Parses cookie name-value map our of |request|.
57CookieMap 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|.
84bool 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
100void 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
113FakeGaia::AccessTokenInfo::AccessTokenInfo()
114  : expires_in(3600) {}
115
116FakeGaia::AccessTokenInfo::~AccessTokenInfo() {}
117
118FakeGaia::MergeSessionParams::MergeSessionParams() {
119}
120
121FakeGaia::MergeSessionParams::~MergeSessionParams() {
122}
123
124FakeGaia::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
132FakeGaia::~FakeGaia() {}
133
134void FakeGaia::SetMergeSessionParams(
135    const MergeSessionParams& params) {
136  merge_session_params_ = params;
137}
138
139void 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
181scoped_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
199void 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
204void 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
210bool 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
219void 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
255void 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
300void 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
308const 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
332void 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
339void 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
373void 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    http_response->AddCustomHeader("Google-Accounts-SAML", "Start");
389  } else if (!merge_session_params_.auth_sid_cookie.empty() &&
390             !merge_session_params_.auth_lsid_cookie.empty()) {
391    SetCookies(http_response,
392               merge_session_params_.auth_sid_cookie,
393               merge_session_params_.auth_lsid_cookie);
394  }
395
396  http_response->set_code(net::HTTP_TEMPORARY_REDIRECT);
397  http_response->AddCustomHeader("Location", redirect_url);
398}
399
400void FakeGaia::HandleSSO(const HttpRequest& request,
401                         BasicHttpResponse* http_response) {
402  if (!merge_session_params_.auth_sid_cookie.empty() &&
403      !merge_session_params_.auth_lsid_cookie.empty()) {
404    SetCookies(http_response,
405               merge_session_params_.auth_sid_cookie,
406               merge_session_params_.auth_lsid_cookie);
407  }
408  std::string relay_state;
409  GetQueryParameter(request.content, "RelayState", &relay_state);
410  std::string redirect_url = relay_state;
411  http_response->set_code(net::HTTP_TEMPORARY_REDIRECT);
412  http_response->AddCustomHeader("Location", redirect_url);
413  http_response->AddCustomHeader("Google-Accounts-SAML", "End");
414}
415
416void FakeGaia::HandleAuthToken(const HttpRequest& request,
417                               BasicHttpResponse* http_response) {
418  std::string scope;
419  GetQueryParameter(request.content, "scope", &scope);
420
421  std::string grant_type;
422  if (!GetQueryParameter(request.content, "grant_type", &grant_type)) {
423    http_response->set_code(net::HTTP_BAD_REQUEST);
424    LOG(ERROR) << "No 'grant_type' param in /o/oauth2/token";
425    return;
426  }
427
428  if (grant_type == "authorization_code") {
429    std::string auth_code;
430    if (!GetQueryParameter(request.content, "code", &auth_code) ||
431        auth_code != merge_session_params_.auth_code) {
432      http_response->set_code(net::HTTP_BAD_REQUEST);
433      LOG(ERROR) << "No 'code' param in /o/oauth2/token";
434      return;
435    }
436
437    if (GaiaConstants::kOAuth1LoginScope != scope) {
438      http_response->set_code(net::HTTP_BAD_REQUEST);
439      LOG(ERROR) << "Invalid scope for /o/oauth2/token - " << scope;
440      return;
441    }
442
443    base::DictionaryValue response_dict;
444    response_dict.SetString("refresh_token",
445                            merge_session_params_.refresh_token);
446    response_dict.SetString("access_token",
447                            merge_session_params_.access_token);
448    response_dict.SetInteger("expires_in", 3600);
449    FormatJSONResponse(response_dict, http_response);
450    return;
451  }
452
453  std::string refresh_token;
454  std::string client_id;
455  if (GetQueryParameter(request.content, "refresh_token", &refresh_token) &&
456      GetQueryParameter(request.content, "client_id", &client_id)) {
457    const AccessTokenInfo* token_info =
458        FindAccessTokenInfo(refresh_token, client_id, scope);
459    if (token_info) {
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      return;
465    }
466  }
467
468  LOG(ERROR) << "Bad request for /o/oauth2/token - "
469              << "refresh_token = " << refresh_token
470              << ", scope = " << scope
471              << ", client_id = " << client_id;
472  http_response->set_code(net::HTTP_BAD_REQUEST);
473}
474
475void FakeGaia::HandleTokenInfo(const HttpRequest& request,
476                               BasicHttpResponse* http_response) {
477  const AccessTokenInfo* token_info = NULL;
478  std::string access_token;
479  if (GetQueryParameter(request.content, "access_token", &access_token)) {
480    for (AccessTokenInfoMap::const_iterator entry(
481             access_token_info_map_.begin());
482         entry != access_token_info_map_.end();
483         ++entry) {
484      if (entry->second.token == access_token) {
485        token_info = &(entry->second);
486        break;
487      }
488    }
489  }
490
491  if (token_info) {
492    base::DictionaryValue response_dict;
493    response_dict.SetString("issued_to", token_info->issued_to);
494    response_dict.SetString("audience", token_info->audience);
495    response_dict.SetString("user_id", token_info->user_id);
496    std::vector<std::string> scope_vector(token_info->scopes.begin(),
497                                          token_info->scopes.end());
498    response_dict.SetString("scope", JoinString(scope_vector, " "));
499    response_dict.SetInteger("expires_in", token_info->expires_in);
500    response_dict.SetString("email", token_info->email);
501    FormatJSONResponse(response_dict, http_response);
502  } else {
503    http_response->set_code(net::HTTP_BAD_REQUEST);
504  }
505}
506
507void FakeGaia::HandleIssueToken(const HttpRequest& request,
508                                BasicHttpResponse* http_response) {
509  std::string access_token;
510  std::string scope;
511  std::string client_id;
512  if (GetAccessToken(request, kAuthHeaderBearer, &access_token) &&
513      GetQueryParameter(request.content, "scope", &scope) &&
514      GetQueryParameter(request.content, "client_id", &client_id)) {
515    const AccessTokenInfo* token_info =
516        FindAccessTokenInfo(access_token, client_id, scope);
517    if (token_info) {
518      base::DictionaryValue response_dict;
519      response_dict.SetString("issueAdvice", "auto");
520      response_dict.SetString("expiresIn",
521                              base::IntToString(token_info->expires_in));
522      response_dict.SetString("token", token_info->token);
523      FormatJSONResponse(response_dict, http_response);
524      return;
525    }
526  }
527  http_response->set_code(net::HTTP_BAD_REQUEST);
528}
529
530void FakeGaia::HandleListAccounts(const HttpRequest& request,
531                                 BasicHttpResponse* http_response) {
532  http_response->set_content(base::StringPrintf(
533      kListAccountsResponseFormat, merge_session_params_.email.c_str()));
534  http_response->set_code(net::HTTP_OK);
535}
536