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 "chrome/browser/extensions/api/identity/gaia_web_auth_flow.h"
6
7#include "base/debug/trace_event.h"
8#include "base/strings/string_number_conversions.h"
9#include "base/strings/string_split.h"
10#include "base/strings/string_util.h"
11#include "base/strings/stringprintf.h"
12#include "chrome/browser/profiles/profile.h"
13#include "chrome/browser/signin/chrome_signin_client_factory.h"
14#include "chrome/browser/signin/profile_oauth2_token_service_factory.h"
15#include "chrome/browser/signin/signin_manager_factory.h"
16#include "components/signin/core/browser/profile_oauth2_token_service.h"
17#include "components/signin/core/browser/signin_manager.h"
18#include "google_apis/gaia/gaia_urls.h"
19#include "net/base/escape.h"
20
21namespace extensions {
22
23GaiaWebAuthFlow::GaiaWebAuthFlow(Delegate* delegate,
24                                 Profile* profile,
25                                 const ExtensionTokenKey* token_key,
26                                 const std::string& oauth2_client_id,
27                                 const std::string& locale)
28    : delegate_(delegate),
29      profile_(profile),
30      account_id_(token_key->account_id) {
31  TRACE_EVENT_ASYNC_BEGIN2("identity",
32                           "GaiaWebAuthFlow",
33                           this,
34                           "extension_id",
35                           token_key->extension_id,
36                           "account_id",
37                           token_key->account_id);
38
39  const char kOAuth2RedirectPathFormat[] = "/%s#";
40  const char kOAuth2AuthorizeFormat[] =
41      "?response_type=token&approval_prompt=force&authuser=0&"
42      "client_id=%s&"
43      "scope=%s&"
44      "origin=chrome-extension://%s/&"
45      "redirect_uri=%s:/%s&"
46      "hl=%s";
47  // Additional parameters to pass if device_id is enabled.
48  const char kOAuth2AuthorizeFormatDeviceIdAddendum[] =
49      "&device_id=%s&"
50      "device_type=chrome";
51
52  std::vector<std::string> scopes(token_key->scopes.begin(),
53                                  token_key->scopes.end());
54  std::vector<std::string> client_id_parts;
55  base::SplitString(oauth2_client_id, '.', &client_id_parts);
56  std::reverse(client_id_parts.begin(), client_id_parts.end());
57  redirect_scheme_ = JoinString(client_id_parts, '.');
58  std::string signin_scoped_device_id;
59  // profile_ can be nullptr in unittests.
60  SigninClient* signin_client =
61      profile_ ? ChromeSigninClientFactory::GetForProfile(profile_) : nullptr;
62  if (signin_client)
63    signin_scoped_device_id = signin_client->GetSigninScopedDeviceId();
64
65  redirect_path_prefix_ = base::StringPrintf(kOAuth2RedirectPathFormat,
66                                             token_key->extension_id.c_str());
67
68  std::string oauth2_authorize_params = base::StringPrintf(
69      kOAuth2AuthorizeFormat,
70      oauth2_client_id.c_str(),
71      net::EscapeUrlEncodedData(JoinString(scopes, ' '), true).c_str(),
72      token_key->extension_id.c_str(),
73      redirect_scheme_.c_str(),
74      token_key->extension_id.c_str(),
75      locale.c_str());
76  if (!signin_scoped_device_id.empty()) {
77    oauth2_authorize_params += base::StringPrintf(
78        kOAuth2AuthorizeFormatDeviceIdAddendum,
79        net::EscapeUrlEncodedData(signin_scoped_device_id, true).c_str());
80  }
81  auth_url_ = GaiaUrls::GetInstance()->oauth2_auth_url().Resolve(
82      oauth2_authorize_params);
83}
84
85GaiaWebAuthFlow::~GaiaWebAuthFlow() {
86  TRACE_EVENT_ASYNC_END0("identity", "GaiaWebAuthFlow", this);
87
88  if (web_flow_)
89    web_flow_.release()->DetachDelegateAndDelete();
90}
91
92void GaiaWebAuthFlow::Start() {
93  ProfileOAuth2TokenService* token_service =
94      ProfileOAuth2TokenServiceFactory::GetForProfile(profile_);
95  ubertoken_fetcher_.reset(new UbertokenFetcher(token_service,
96                                                this,
97                                                profile_->GetRequestContext()));
98  ubertoken_fetcher_->StartFetchingToken(account_id_);
99}
100
101void GaiaWebAuthFlow::OnUbertokenSuccess(const std::string& token) {
102  TRACE_EVENT_ASYNC_STEP_PAST0(
103      "identity", "GaiaWebAuthFlow", this, "OnUbertokenSuccess");
104
105  const char kMergeSessionQueryFormat[] = "?uberauth=%s&"
106                                          "continue=%s&"
107                                          "source=appsv2";
108
109  std::string merge_query = base::StringPrintf(
110      kMergeSessionQueryFormat,
111      net::EscapeUrlEncodedData(token, true).c_str(),
112      net::EscapeUrlEncodedData(auth_url_.spec(), true).c_str());
113  GURL merge_url(
114      GaiaUrls::GetInstance()->merge_session_url().Resolve(merge_query));
115
116  web_flow_ = CreateWebAuthFlow(merge_url);
117  web_flow_->Start();
118}
119
120void GaiaWebAuthFlow::OnUbertokenFailure(const GoogleServiceAuthError& error) {
121  TRACE_EVENT_ASYNC_STEP_PAST1("identity",
122                               "GaiaWebAuthFlow",
123                               this,
124                               "OnUbertokenSuccess",
125                               "error",
126                               error.ToString());
127
128  DVLOG(1) << "OnUbertokenFailure: " << error.error_message();
129  delegate_->OnGaiaFlowFailure(
130      GaiaWebAuthFlow::SERVICE_AUTH_ERROR, error, std::string());
131}
132
133void GaiaWebAuthFlow::OnAuthFlowFailure(WebAuthFlow::Failure failure) {
134  GaiaWebAuthFlow::Failure gaia_failure;
135
136  switch (failure) {
137    case WebAuthFlow::WINDOW_CLOSED:
138      gaia_failure = GaiaWebAuthFlow::WINDOW_CLOSED;
139      break;
140    case WebAuthFlow::LOAD_FAILED:
141      DVLOG(1) << "OnAuthFlowFailure LOAD_FAILED";
142      gaia_failure = GaiaWebAuthFlow::LOAD_FAILED;
143      break;
144    default:
145      NOTREACHED() << "Unexpected error from web auth flow: " << failure;
146      gaia_failure = GaiaWebAuthFlow::LOAD_FAILED;
147      break;
148  }
149
150  TRACE_EVENT_ASYNC_STEP_PAST1("identity",
151                               "GaiaWebAuthFlow",
152                               this,
153                               "OnAuthFlowFailure",
154                               "error",
155                               gaia_failure);
156
157  delegate_->OnGaiaFlowFailure(
158      gaia_failure,
159      GoogleServiceAuthError(GoogleServiceAuthError::NONE),
160      std::string());
161}
162
163void GaiaWebAuthFlow::OnAuthFlowURLChange(const GURL& url) {
164  TRACE_EVENT_ASYNC_STEP_PAST0("identity",
165                               "GaiaWebAuthFlow",
166                               this,
167                               "OnAuthFlowURLChange");
168
169  const char kOAuth2RedirectAccessTokenKey[] = "access_token";
170  const char kOAuth2RedirectErrorKey[] = "error";
171  const char kOAuth2ExpiresInKey[] = "expires_in";
172
173  // The format of the target URL is:
174  //     reversed.oauth.client.id:/extensionid#access_token=TOKEN
175  //
176  // Because there is no double slash, everything after the scheme is
177  // interpreted as a path, including the fragment.
178
179  if (url.scheme() == redirect_scheme_ && !url.has_host() && !url.has_port() &&
180      StartsWithASCII(url.GetContent(), redirect_path_prefix_, true)) {
181    web_flow_.release()->DetachDelegateAndDelete();
182
183    std::string fragment = url.GetContent().substr(
184        redirect_path_prefix_.length(), std::string::npos);
185    base::StringPairs pairs;
186    base::SplitStringIntoKeyValuePairs(fragment, '=', '&', &pairs);
187    std::string access_token;
188    std::string error;
189    std::string expiration;
190
191    for (base::StringPairs::iterator it = pairs.begin();
192         it != pairs.end();
193         ++it) {
194      if (it->first == kOAuth2RedirectAccessTokenKey)
195        access_token = it->second;
196      else if (it->first == kOAuth2RedirectErrorKey)
197        error = it->second;
198      else if (it->first == kOAuth2ExpiresInKey)
199        expiration = it->second;
200    }
201
202    if (access_token.empty() && error.empty()) {
203      delegate_->OnGaiaFlowFailure(
204          GaiaWebAuthFlow::INVALID_REDIRECT,
205          GoogleServiceAuthError(GoogleServiceAuthError::NONE),
206          std::string());
207    } else if (!error.empty()) {
208      delegate_->OnGaiaFlowFailure(
209          GaiaWebAuthFlow::OAUTH_ERROR,
210          GoogleServiceAuthError(GoogleServiceAuthError::NONE),
211          error);
212    } else {
213      delegate_->OnGaiaFlowCompleted(access_token, expiration);
214    }
215  }
216}
217
218void GaiaWebAuthFlow::OnAuthFlowTitleChange(const std::string& title) {
219  // On the final page the title will be "Loading <redirect-url>".
220  // Treat it as though we'd really been redirected to <redirect-url>.
221  const char kRedirectPrefix[] = "Loading ";
222  std::string prefix(kRedirectPrefix);
223
224  if (StartsWithASCII(title, prefix, true)) {
225    GURL url(title.substr(prefix.length(), std::string::npos));
226    if (url.is_valid())
227      OnAuthFlowURLChange(url);
228  }
229}
230
231scoped_ptr<WebAuthFlow> GaiaWebAuthFlow::CreateWebAuthFlow(GURL url) {
232  return scoped_ptr<WebAuthFlow>(new WebAuthFlow(this,
233                                                 profile_,
234                                                 url,
235                                                 WebAuthFlow::INTERACTIVE));
236}
237
238}  // namespace extensions
239