oauth2_access_token_fetcher.cc revision f2477e01787aa58f445919b809d89e252beef54f
1// Copyright (c) 2012 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/oauth2_access_token_fetcher.h"
6
7#include <algorithm>
8#include <string>
9#include <vector>
10
11#include "base/json/json_reader.h"
12#include "base/metrics/histogram.h"
13#include "base/metrics/sparse_histogram.h"
14#include "base/strings/string_util.h"
15#include "base/strings/stringprintf.h"
16#include "base/time/time.h"
17#include "base/values.h"
18#include "google_apis/gaia/gaia_urls.h"
19#include "google_apis/gaia/google_service_auth_error.h"
20#include "net/base/escape.h"
21#include "net/base/load_flags.h"
22#include "net/http/http_status_code.h"
23#include "net/url_request/url_fetcher.h"
24#include "net/url_request/url_request_context_getter.h"
25#include "net/url_request/url_request_status.h"
26
27using net::ResponseCookies;
28using net::URLFetcher;
29using net::URLFetcherDelegate;
30using net::URLRequestContextGetter;
31using net::URLRequestStatus;
32
33namespace {
34static const char kGetAccessTokenBodyFormat[] =
35    "client_id=%s&"
36    "client_secret=%s&"
37    "grant_type=refresh_token&"
38    "refresh_token=%s";
39
40static const char kGetAccessTokenBodyWithScopeFormat[] =
41    "client_id=%s&"
42    "client_secret=%s&"
43    "grant_type=refresh_token&"
44    "refresh_token=%s&"
45    "scope=%s";
46
47static const char kAccessTokenKey[] = "access_token";
48static const char kExpiresInKey[] = "expires_in";
49static const char kErrorKey[] = "error";
50
51// Enumerated constants for logging server responses on 400 errors, matching
52// RFC 6749.
53enum OAuth2ErrorCodesForHistogram {
54   OAUTH2_ACCESS_ERROR_INVALID_REQUEST = 0,
55   OAUTH2_ACCESS_ERROR_INVALID_CLIENT,
56   OAUTH2_ACCESS_ERROR_INVALID_GRANT,
57   OAUTH2_ACCESS_ERROR_UNAUTHORIZED_CLIENT,
58   OAUTH2_ACCESS_ERROR_UNSUPPORTED_GRANT_TYPE,
59   OAUTH2_ACCESS_ERROR_INVALID_SCOPE,
60   OAUTH2_ACCESS_ERROR_UNKNOWN,
61   OAUTH2_ACCESS_ERROR_COUNT
62};
63
64OAuth2ErrorCodesForHistogram OAuth2ErrorToHistogramValue(
65    const std::string& error) {
66  if (error == "invalid_request")
67    return OAUTH2_ACCESS_ERROR_INVALID_REQUEST;
68  else if (error == "invalid_client")
69    return OAUTH2_ACCESS_ERROR_INVALID_CLIENT;
70  else if (error == "invalid_grant")
71    return OAUTH2_ACCESS_ERROR_INVALID_GRANT;
72  else if (error == "unauthorized_client")
73    return OAUTH2_ACCESS_ERROR_UNAUTHORIZED_CLIENT;
74  else if (error == "unsupported_grant_type")
75    return OAUTH2_ACCESS_ERROR_UNSUPPORTED_GRANT_TYPE;
76  else if (error == "invalid_scope")
77    return OAUTH2_ACCESS_ERROR_INVALID_SCOPE;
78
79  return OAUTH2_ACCESS_ERROR_UNKNOWN;
80}
81
82static GoogleServiceAuthError CreateAuthError(URLRequestStatus status) {
83  CHECK(!status.is_success());
84  if (status.status() == URLRequestStatus::CANCELED) {
85    return GoogleServiceAuthError(GoogleServiceAuthError::REQUEST_CANCELED);
86  } else {
87    DLOG(WARNING) << "Could not reach Google Accounts servers: errno "
88                  << status.error();
89    return GoogleServiceAuthError::FromConnectionError(status.error());
90  }
91}
92
93static URLFetcher* CreateFetcher(URLRequestContextGetter* getter,
94                                 const GURL& url,
95                                 const std::string& body,
96                                 URLFetcherDelegate* delegate) {
97  bool empty_body = body.empty();
98  URLFetcher* result = net::URLFetcher::Create(
99      0, url,
100      empty_body ? URLFetcher::GET : URLFetcher::POST,
101      delegate);
102
103  result->SetRequestContext(getter);
104  result->SetLoadFlags(net::LOAD_DO_NOT_SEND_COOKIES |
105                       net::LOAD_DO_NOT_SAVE_COOKIES);
106  // Fetchers are sometimes cancelled because a network change was detected,
107  // especially at startup and after sign-in on ChromeOS. Retrying once should
108  // be enough in those cases; let the fetcher retry up to 3 times just in case.
109  // http://crbug.com/163710
110  result->SetAutomaticallyRetryOnNetworkChanges(3);
111
112  if (!empty_body)
113    result->SetUploadData("application/x-www-form-urlencoded", body);
114
115  return result;
116}
117}  // namespace
118
119OAuth2AccessTokenFetcher::OAuth2AccessTokenFetcher(
120    OAuth2AccessTokenConsumer* consumer,
121    URLRequestContextGetter* getter)
122    : consumer_(consumer),
123      getter_(getter),
124      state_(INITIAL) { }
125
126OAuth2AccessTokenFetcher::~OAuth2AccessTokenFetcher() { }
127
128void OAuth2AccessTokenFetcher::CancelRequest() {
129  fetcher_.reset();
130}
131
132void OAuth2AccessTokenFetcher::Start(const std::string& client_id,
133                                     const std::string& client_secret,
134                                     const std::string& refresh_token,
135                                     const std::vector<std::string>& scopes) {
136  client_id_ = client_id;
137  client_secret_ = client_secret;
138  refresh_token_ = refresh_token;
139  scopes_ = scopes;
140  StartGetAccessToken();
141}
142
143void OAuth2AccessTokenFetcher::StartGetAccessToken() {
144  CHECK_EQ(INITIAL, state_);
145  state_ = GET_ACCESS_TOKEN_STARTED;
146  fetcher_.reset(CreateFetcher(
147      getter_,
148      MakeGetAccessTokenUrl(),
149      MakeGetAccessTokenBody(
150          client_id_, client_secret_, refresh_token_, scopes_),
151      this));
152  fetcher_->Start();  // OnURLFetchComplete will be called.
153}
154
155void OAuth2AccessTokenFetcher::EndGetAccessToken(
156    const net::URLFetcher* source) {
157  CHECK_EQ(GET_ACCESS_TOKEN_STARTED, state_);
158  state_ = GET_ACCESS_TOKEN_DONE;
159
160  URLRequestStatus status = source->GetStatus();
161  int histogram_value = status.is_success() ? source->GetResponseCode() :
162                                              status.error();
163  UMA_HISTOGRAM_SPARSE_SLOWLY("Gaia.ResponseCodesForOAuth2AccessToken",
164                              histogram_value);
165  if (!status.is_success()) {
166    OnGetTokenFailure(CreateAuthError(status));
167    return;
168  }
169
170  switch (source->GetResponseCode()) {
171    case net::HTTP_OK:
172      break;
173    case net::HTTP_FORBIDDEN:
174    case net::HTTP_INTERNAL_SERVER_ERROR:
175      // HTTP_FORBIDDEN (403) is treated as temporary error, because it may be
176      // '403 Rate Limit Exeeded.' 500 is always treated as transient.
177      OnGetTokenFailure(GoogleServiceAuthError(
178          GoogleServiceAuthError::SERVICE_UNAVAILABLE));
179      return;
180    case net::HTTP_BAD_REQUEST: {
181      // HTTP_BAD_REQUEST (400) usually contains error as per
182      // http://tools.ietf.org/html/rfc6749#section-5.2.
183      std::string gaia_error;
184      if (!ParseGetAccessTokenFailureResponse(source, &gaia_error)) {
185        OnGetTokenFailure(GoogleServiceAuthError(
186            GoogleServiceAuthError::SERVICE_ERROR));
187        return;
188      }
189
190      OAuth2ErrorCodesForHistogram access_error(OAuth2ErrorToHistogramValue(
191          gaia_error));
192      UMA_HISTOGRAM_ENUMERATION("Gaia.BadRequestTypeForOAuth2AccessToken",
193                                access_error, OAUTH2_ACCESS_ERROR_COUNT);
194
195      OnGetTokenFailure(access_error == OAUTH2_ACCESS_ERROR_INVALID_GRANT ?
196          GoogleServiceAuthError(
197              GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS) :
198          GoogleServiceAuthError(
199              GoogleServiceAuthError::SERVICE_ERROR));
200      return;
201    }
202    default:
203      // The other errors are treated as permanent error.
204      OnGetTokenFailure(GoogleServiceAuthError(
205          GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS));
206      return;
207  }
208
209  // The request was successfully fetched and it returned OK.
210  // Parse out the access token and the expiration time.
211  std::string access_token;
212  int expires_in;
213  if (!ParseGetAccessTokenSuccessResponse(
214          source, &access_token, &expires_in)) {
215    DLOG(WARNING) << "Response doesn't match expected format";
216    OnGetTokenFailure(
217        GoogleServiceAuthError(GoogleServiceAuthError::SERVICE_UNAVAILABLE));
218    return;
219  }
220  // The token will expire in |expires_in| seconds. Take a 10% error margin to
221  // prevent reusing a token too close to its expiration date.
222  OnGetTokenSuccess(
223      access_token,
224      base::Time::Now() + base::TimeDelta::FromSeconds(9 * expires_in / 10));
225}
226
227void OAuth2AccessTokenFetcher::OnGetTokenSuccess(
228    const std::string& access_token,
229    const base::Time& expiration_time) {
230  consumer_->OnGetTokenSuccess(access_token, expiration_time);
231}
232
233void OAuth2AccessTokenFetcher::OnGetTokenFailure(
234    const GoogleServiceAuthError& error) {
235  state_ = ERROR_STATE;
236  consumer_->OnGetTokenFailure(error);
237}
238
239void OAuth2AccessTokenFetcher::OnURLFetchComplete(
240    const net::URLFetcher* source) {
241  CHECK(source);
242  CHECK(state_ == GET_ACCESS_TOKEN_STARTED);
243  EndGetAccessToken(source);
244}
245
246// static
247GURL OAuth2AccessTokenFetcher::MakeGetAccessTokenUrl() {
248  return GaiaUrls::GetInstance()->oauth2_token_url();
249}
250
251// static
252std::string OAuth2AccessTokenFetcher::MakeGetAccessTokenBody(
253    const std::string& client_id,
254    const std::string& client_secret,
255    const std::string& refresh_token,
256    const std::vector<std::string>& scopes) {
257  std::string enc_client_id = net::EscapeUrlEncodedData(client_id, true);
258  std::string enc_client_secret =
259      net::EscapeUrlEncodedData(client_secret, true);
260  std::string enc_refresh_token =
261      net::EscapeUrlEncodedData(refresh_token, true);
262  if (scopes.empty()) {
263    return base::StringPrintf(
264        kGetAccessTokenBodyFormat,
265        enc_client_id.c_str(),
266        enc_client_secret.c_str(),
267        enc_refresh_token.c_str());
268  } else {
269    std::string scopes_string = JoinString(scopes, ' ');
270    return base::StringPrintf(
271        kGetAccessTokenBodyWithScopeFormat,
272        enc_client_id.c_str(),
273        enc_client_secret.c_str(),
274        enc_refresh_token.c_str(),
275        net::EscapeUrlEncodedData(scopes_string, true).c_str());
276  }
277}
278
279scoped_ptr<base::DictionaryValue> ParseGetAccessTokenResponse(
280    const net::URLFetcher* source) {
281  CHECK(source);
282
283  std::string data;
284  source->GetResponseAsString(&data);
285  scoped_ptr<base::Value> value(base::JSONReader::Read(data));
286  if (!value.get() || value->GetType() != base::Value::TYPE_DICTIONARY)
287    value.reset();
288
289  return scoped_ptr<base::DictionaryValue>(
290      static_cast<base::DictionaryValue*>(value.release()));
291}
292
293// static
294bool OAuth2AccessTokenFetcher::ParseGetAccessTokenSuccessResponse(
295    const net::URLFetcher* source,
296    std::string* access_token,
297    int* expires_in) {
298  CHECK(access_token);
299  scoped_ptr<base::DictionaryValue> value = ParseGetAccessTokenResponse(
300      source);
301  if (value.get() == NULL)
302    return false;
303
304  return value->GetString(kAccessTokenKey, access_token) &&
305      value->GetInteger(kExpiresInKey, expires_in);
306}
307
308// static
309bool OAuth2AccessTokenFetcher::ParseGetAccessTokenFailureResponse(
310    const net::URLFetcher* source,
311    std::string* error) {
312  CHECK(error);
313  scoped_ptr<base::DictionaryValue> value = ParseGetAccessTokenResponse(
314      source);
315  if (value.get() == NULL)
316    return false;
317  return value->GetString(kErrorKey, error);
318}
319