blob: 158249ed5f60379029921899a7bb468b31261563 [file] [log] [blame]
C.J. Collier37141e42020-02-13 13:49:49 -08001# Copyright 2016 Google LLC
Jon Wayne Parrott10ec7e92016-10-17 10:46:38 -07002#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""OAuth 2.0 Credentials.
16
17This module provides credentials based on OAuth 2.0 access and refresh tokens.
18These credentials usually access resources on behalf of a user (resource
19owner).
20
21Specifically, this is intended to use access tokens acquired using the
22`Authorization Code grant`_ and can refresh those tokens using a
23optional `refresh token`_.
24
25Obtaining the initial access and refresh token is outside of the scope of this
26module. Consult `rfc6749 section 4.1`_ for complete details on the
27Authorization Code grant flow.
28
29.. _Authorization Code grant: https://tools.ietf.org/html/rfc6749#section-1.3.1
30.. _refresh token: https://tools.ietf.org/html/rfc6749#section-6
31.. _rfc6749 section 4.1: https://tools.ietf.org/html/rfc6749#section-4.1
32"""
33
wesley chund0e0aba2020-09-17 09:18:55 -070034from datetime import datetime
Hiranya Jayathilaka23c88f72017-12-05 09:29:59 -080035import io
36import json
37
38import six
39
arithmetic1728772dac62020-03-27 14:34:13 -070040from google.auth import _cloud_sdk
Jon Wayne Parrott10ec7e92016-10-17 10:46:38 -070041from google.auth import _helpers
42from google.auth import credentials
Thea Flowers118c0482018-05-24 13:34:07 -070043from google.auth import exceptions
arithmetic172882293fe2021-04-14 11:22:13 -070044from google.oauth2 import reauth
Jon Wayne Parrott10ec7e92016-10-17 10:46:38 -070045
46
Hiranya Jayathilaka23c88f72017-12-05 09:29:59 -080047# The Google OAuth 2.0 token endpoint. Used for authorized user credentials.
Bu Sun Kim9eec0912019-10-21 17:04:21 -070048_GOOGLE_OAUTH2_TOKEN_ENDPOINT = "https://oauth2.googleapis.com/token"
Hiranya Jayathilaka23c88f72017-12-05 09:29:59 -080049
50
Bu Sun Kim41599ae2020-09-02 12:55:42 -060051class Credentials(credentials.ReadOnlyScoped, credentials.CredentialsWithQuotaProject):
Bu Sun Kimb12488c2020-06-10 13:44:07 -070052 """Credentials using OAuth 2.0 access and refresh tokens.
53
54 The credentials are considered immutable. If you want to modify the
55 quota project, use :meth:`with_quota_project` or ::
56
57 credentials = credentials.with_quota_project('myproject-123)
arithmetic172882293fe2021-04-14 11:22:13 -070058
59 If reauth is enabled, `pyu2f` dependency has to be installed in order to use security
60 key reauth feature. Dependency can be installed via `pip install pyu2f` or `pip install
61 google-auth[reauth]`.
Bu Sun Kimb12488c2020-06-10 13:44:07 -070062 """
Jon Wayne Parrott10ec7e92016-10-17 10:46:38 -070063
Bu Sun Kim9eec0912019-10-21 17:04:21 -070064 def __init__(
65 self,
66 token,
67 refresh_token=None,
68 id_token=None,
69 token_uri=None,
70 client_id=None,
71 client_secret=None,
72 scopes=None,
Bu Sun Kimbf5ce0c2021-02-01 15:17:49 -070073 default_scopes=None,
Bu Sun Kim32d71a52019-12-18 11:30:46 -080074 quota_project_id=None,
wesley chund0e0aba2020-09-17 09:18:55 -070075 expiry=None,
arithmetic172882293fe2021-04-14 11:22:13 -070076 rapt_token=None,
bojeil-googleec2fb182021-07-22 10:01:31 -070077 refresh_handler=None,
Bu Sun Kim9eec0912019-10-21 17:04:21 -070078 ):
Jon Wayne Parrott10ec7e92016-10-17 10:46:38 -070079 """
80 Args:
81 token (Optional(str)): The OAuth 2.0 access token. Can be None
82 if refresh information is provided.
83 refresh_token (str): The OAuth 2.0 refresh token. If specified,
84 credentials can be refreshed.
Jon Wayne Parrott26a16372017-03-28 13:03:33 -070085 id_token (str): The Open ID Connect ID Token.
Jon Wayne Parrott10ec7e92016-10-17 10:46:38 -070086 token_uri (str): The OAuth 2.0 authorization server's token
87 endpoint URI. Must be specified for refresh, can be left as
88 None if the token can not be refreshed.
89 client_id (str): The OAuth 2.0 client ID. Must be specified for
90 refresh, can be left as None if the token can not be refreshed.
91 client_secret(str): The OAuth 2.0 client secret. Must be specified
92 for refresh, can be left as None if the token can not be
93 refreshed.
Eugene W. Foley49a18c42019-05-22 13:50:38 -040094 scopes (Sequence[str]): The scopes used to obtain authorization.
95 This parameter is used by :meth:`has_scopes`. OAuth 2.0
96 credentials can not request additional scopes after
97 authorization. The scopes must be derivable from the refresh
98 token if refresh information is provided (e.g. The refresh
99 token scopes are a superset of this or contain a wild card
100 scope like 'https://www.googleapis.com/auth/any-api').
Bu Sun Kimbf5ce0c2021-02-01 15:17:49 -0700101 default_scopes (Sequence[str]): Default scopes passed by a
102 Google client library. Use 'scopes' for user-defined scopes.
Bu Sun Kim32d71a52019-12-18 11:30:46 -0800103 quota_project_id (Optional[str]): The project ID used for quota and billing.
104 This project may be different from the project used to
105 create the credentials.
arithmetic172882293fe2021-04-14 11:22:13 -0700106 rapt_token (Optional[str]): The reauth Proof Token.
bojeil-googleec2fb182021-07-22 10:01:31 -0700107 refresh_handler (Optional[Callable[[google.auth.transport.Request, Sequence[str]], [str, datetime]]]):
108 A callable which takes in the HTTP request callable and the list of
109 OAuth scopes and when called returns an access token string for the
110 requested scopes and its expiry datetime. This is useful when no
111 refresh tokens are provided and tokens are obtained by calling
112 some external process on demand. It is particularly useful for
113 retrieving downscoped tokens from a token broker.
Jon Wayne Parrott10ec7e92016-10-17 10:46:38 -0700114 """
115 super(Credentials, self).__init__()
116 self.token = token
wesley chund0e0aba2020-09-17 09:18:55 -0700117 self.expiry = expiry
Jon Wayne Parrott10ec7e92016-10-17 10:46:38 -0700118 self._refresh_token = refresh_token
Jon Wayne Parrott26a16372017-03-28 13:03:33 -0700119 self._id_token = id_token
Jon Wayne Parrott10ec7e92016-10-17 10:46:38 -0700120 self._scopes = scopes
Bu Sun Kimbf5ce0c2021-02-01 15:17:49 -0700121 self._default_scopes = default_scopes
Jon Wayne Parrott10ec7e92016-10-17 10:46:38 -0700122 self._token_uri = token_uri
123 self._client_id = client_id
124 self._client_secret = client_secret
Bu Sun Kim32d71a52019-12-18 11:30:46 -0800125 self._quota_project_id = quota_project_id
arithmetic172882293fe2021-04-14 11:22:13 -0700126 self._rapt_token = rapt_token
bojeil-googleec2fb182021-07-22 10:01:31 -0700127 self.refresh_handler = refresh_handler
Bu Sun Kim32d71a52019-12-18 11:30:46 -0800128
129 def __getstate__(self):
130 """A __getstate__ method must exist for the __setstate__ to be called
131 This is identical to the default implementation.
132 See https://docs.python.org/3.7/library/pickle.html#object.__setstate__
133 """
bojeil-googleec2fb182021-07-22 10:01:31 -0700134 state_dict = self.__dict__.copy()
135 # Remove _refresh_handler function as there are limitations pickling and
136 # unpickling certain callables (lambda, functools.partial instances)
137 # because they need to be importable.
138 # Instead, the refresh_handler setter should be used to repopulate this.
139 del state_dict["_refresh_handler"]
140 return state_dict
Bu Sun Kim32d71a52019-12-18 11:30:46 -0800141
142 def __setstate__(self, d):
143 """Credentials pickled with older versions of the class do not have
144 all the attributes."""
145 self.token = d.get("token")
146 self.expiry = d.get("expiry")
147 self._refresh_token = d.get("_refresh_token")
148 self._id_token = d.get("_id_token")
149 self._scopes = d.get("_scopes")
Bu Sun Kimbf5ce0c2021-02-01 15:17:49 -0700150 self._default_scopes = d.get("_default_scopes")
Bu Sun Kim32d71a52019-12-18 11:30:46 -0800151 self._token_uri = d.get("_token_uri")
152 self._client_id = d.get("_client_id")
153 self._client_secret = d.get("_client_secret")
154 self._quota_project_id = d.get("_quota_project_id")
arithmetic172882293fe2021-04-14 11:22:13 -0700155 self._rapt_token = d.get("_rapt_token")
bojeil-googleec2fb182021-07-22 10:01:31 -0700156 # The refresh_handler setter should be used to repopulate this.
157 self._refresh_handler = None
Jon Wayne Parrott10ec7e92016-10-17 10:46:38 -0700158
159 @property
Jon Wayne Parrott2d0549a2017-03-01 09:27:16 -0800160 def refresh_token(self):
161 """Optional[str]: The OAuth 2.0 refresh token."""
162 return self._refresh_token
163
164 @property
wesley chund0e0aba2020-09-17 09:18:55 -0700165 def scopes(self):
166 """Optional[str]: The OAuth 2.0 permission scopes."""
167 return self._scopes
168
169 @property
Jon Wayne Parrott2d0549a2017-03-01 09:27:16 -0800170 def token_uri(self):
171 """Optional[str]: The OAuth 2.0 authorization server's token endpoint
172 URI."""
173 return self._token_uri
174
175 @property
Jon Wayne Parrott26a16372017-03-28 13:03:33 -0700176 def id_token(self):
177 """Optional[str]: The Open ID Connect ID Token.
178
179 Depending on the authorization server and the scopes requested, this
180 may be populated when credentials are obtained and updated when
181 :meth:`refresh` is called. This token is a JWT. It can be verified
182 and decoded using :func:`google.oauth2.id_token.verify_oauth2_token`.
183 """
184 return self._id_token
185
186 @property
Jon Wayne Parrott2d0549a2017-03-01 09:27:16 -0800187 def client_id(self):
188 """Optional[str]: The OAuth 2.0 client ID."""
189 return self._client_id
190
191 @property
192 def client_secret(self):
193 """Optional[str]: The OAuth 2.0 client secret."""
194 return self._client_secret
195
196 @property
Jon Wayne Parrott10ec7e92016-10-17 10:46:38 -0700197 def requires_scopes(self):
198 """False: OAuth 2.0 credentials have their scopes set when
199 the initial token is requested and can not be changed."""
200 return False
201
arithmetic172882293fe2021-04-14 11:22:13 -0700202 @property
203 def rapt_token(self):
204 """Optional[str]: The reauth Proof Token."""
205 return self._rapt_token
206
bojeil-googleec2fb182021-07-22 10:01:31 -0700207 @property
208 def refresh_handler(self):
209 """Returns the refresh handler if available.
210
211 Returns:
212 Optional[Callable[[google.auth.transport.Request, Sequence[str]], [str, datetime]]]:
213 The current refresh handler.
214 """
215 return self._refresh_handler
216
217 @refresh_handler.setter
218 def refresh_handler(self, value):
219 """Updates the current refresh handler.
220
221 Args:
222 value (Optional[Callable[[google.auth.transport.Request, Sequence[str]], [str, datetime]]]):
223 The updated value of the refresh handler.
224
225 Raises:
226 TypeError: If the value is not a callable or None.
227 """
228 if not callable(value) and value is not None:
229 raise TypeError("The provided refresh_handler is not a callable or None.")
230 self._refresh_handler = value
231
Bu Sun Kim41599ae2020-09-02 12:55:42 -0600232 @_helpers.copy_docstring(credentials.CredentialsWithQuotaProject)
Bu Sun Kimb12488c2020-06-10 13:44:07 -0700233 def with_quota_project(self, quota_project_id):
Bu Sun Kimb12488c2020-06-10 13:44:07 -0700234
Bu Sun Kimb12488c2020-06-10 13:44:07 -0700235 return self.__class__(
236 self.token,
237 refresh_token=self.refresh_token,
238 id_token=self.id_token,
239 token_uri=self.token_uri,
240 client_id=self.client_id,
241 client_secret=self.client_secret,
242 scopes=self.scopes,
Bu Sun Kimbf5ce0c2021-02-01 15:17:49 -0700243 default_scopes=self.default_scopes,
Bu Sun Kimb12488c2020-06-10 13:44:07 -0700244 quota_project_id=quota_project_id,
arithmetic172882293fe2021-04-14 11:22:13 -0700245 rapt_token=self.rapt_token,
Bu Sun Kimb12488c2020-06-10 13:44:07 -0700246 )
247
Jon Wayne Parrott10ec7e92016-10-17 10:46:38 -0700248 @_helpers.copy_docstring(credentials.Credentials)
249 def refresh(self, request):
bojeil-googleec2fb182021-07-22 10:01:31 -0700250 scopes = self._scopes if self._scopes is not None else self._default_scopes
251 # Use refresh handler if available and no refresh token is
252 # available. This is useful in general when tokens are obtained by calling
253 # some external process on demand. It is particularly useful for retrieving
254 # downscoped tokens from a token broker.
255 if self._refresh_token is None and self.refresh_handler:
256 token, expiry = self.refresh_handler(request, scopes=scopes)
257 # Validate returned data.
258 if not isinstance(token, str):
259 raise exceptions.RefreshError(
260 "The refresh_handler returned token is not a string."
261 )
262 if not isinstance(expiry, datetime):
263 raise exceptions.RefreshError(
264 "The refresh_handler returned expiry is not a datetime object."
265 )
266 if _helpers.utcnow() >= expiry - _helpers.CLOCK_SKEW:
267 raise exceptions.RefreshError(
268 "The credentials returned by the refresh_handler are "
269 "already expired."
270 )
271 self.token = token
272 self.expiry = expiry
273 return
274
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700275 if (
276 self._refresh_token is None
277 or self._token_uri is None
278 or self._client_id is None
279 or self._client_secret is None
280 ):
Thea Flowers118c0482018-05-24 13:34:07 -0700281 raise exceptions.RefreshError(
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700282 "The credentials do not contain the necessary fields need to "
283 "refresh the access token. You must specify refresh_token, "
284 "token_uri, client_id, and client_secret."
285 )
Thea Flowers118c0482018-05-24 13:34:07 -0700286
arithmetic172882293fe2021-04-14 11:22:13 -0700287 (
288 access_token,
289 refresh_token,
290 expiry,
291 grant_response,
292 rapt_token,
293 ) = reauth.refresh_grant(
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700294 request,
295 self._token_uri,
296 self._refresh_token,
297 self._client_id,
298 self._client_secret,
arithmetic172882293fe2021-04-14 11:22:13 -0700299 scopes=scopes,
300 rapt_token=self._rapt_token,
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700301 )
Jon Wayne Parrott10ec7e92016-10-17 10:46:38 -0700302
303 self.token = access_token
304 self.expiry = expiry
305 self._refresh_token = refresh_token
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700306 self._id_token = grant_response.get("id_token")
arithmetic172882293fe2021-04-14 11:22:13 -0700307 self._rapt_token = rapt_token
Hiranya Jayathilaka23c88f72017-12-05 09:29:59 -0800308
arithmetic172882293fe2021-04-14 11:22:13 -0700309 if scopes and "scope" in grant_response:
Bu Sun Kimbf5ce0c2021-02-01 15:17:49 -0700310 requested_scopes = frozenset(scopes)
arithmetic172882293fe2021-04-14 11:22:13 -0700311 granted_scopes = frozenset(grant_response["scope"].split())
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700312 scopes_requested_but_not_granted = requested_scopes - granted_scopes
Eugene W. Foley49a18c42019-05-22 13:50:38 -0400313 if scopes_requested_but_not_granted:
314 raise exceptions.RefreshError(
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700315 "Not all requested scopes were granted by the "
316 "authorization server, missing scopes {}.".format(
317 ", ".join(scopes_requested_but_not_granted)
318 )
319 )
Eugene W. Foley49a18c42019-05-22 13:50:38 -0400320
Hiranya Jayathilaka23c88f72017-12-05 09:29:59 -0800321 @classmethod
322 def from_authorized_user_info(cls, info, scopes=None):
323 """Creates a Credentials instance from parsed authorized user info.
324
325 Args:
326 info (Mapping[str, str]): The authorized user info in Google
327 format.
328 scopes (Sequence[str]): Optional list of scopes to include in the
329 credentials.
330
331 Returns:
332 google.oauth2.credentials.Credentials: The constructed
333 credentials.
334
335 Raises:
336 ValueError: If the info is not in the expected format.
337 """
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700338 keys_needed = set(("refresh_token", "client_id", "client_secret"))
Hiranya Jayathilaka23c88f72017-12-05 09:29:59 -0800339 missing = keys_needed.difference(six.iterkeys(info))
340
341 if missing:
342 raise ValueError(
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700343 "Authorized user info was not in the expected format, missing "
344 "fields {}.".format(", ".join(missing))
345 )
Hiranya Jayathilaka23c88f72017-12-05 09:29:59 -0800346
wesley chund0e0aba2020-09-17 09:18:55 -0700347 # access token expiry (datetime obj); auto-expire if not saved
348 expiry = info.get("expiry")
349 if expiry:
350 expiry = datetime.strptime(
351 expiry.rstrip("Z").split(".")[0], "%Y-%m-%dT%H:%M:%S"
352 )
353 else:
354 expiry = _helpers.utcnow() - _helpers.CLOCK_SKEW
355
356 # process scopes, which needs to be a seq
357 if scopes is None and "scopes" in info:
358 scopes = info.get("scopes")
359 if isinstance(scopes, str):
360 scopes = scopes.split(" ")
361
Emile Caron530f5f92019-07-26 01:23:25 +0200362 return cls(
wesley chund0e0aba2020-09-17 09:18:55 -0700363 token=info.get("token"),
364 refresh_token=info.get("refresh_token"),
365 token_uri=_GOOGLE_OAUTH2_TOKEN_ENDPOINT, # always overrides
Hiranya Jayathilaka23c88f72017-12-05 09:29:59 -0800366 scopes=scopes,
wesley chund0e0aba2020-09-17 09:18:55 -0700367 client_id=info.get("client_id"),
368 client_secret=info.get("client_secret"),
369 quota_project_id=info.get("quota_project_id"), # may not exist
370 expiry=expiry,
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700371 )
Hiranya Jayathilaka23c88f72017-12-05 09:29:59 -0800372
373 @classmethod
374 def from_authorized_user_file(cls, filename, scopes=None):
375 """Creates a Credentials instance from an authorized user json file.
376
377 Args:
378 filename (str): The path to the authorized user json file.
379 scopes (Sequence[str]): Optional list of scopes to include in the
380 credentials.
381
382 Returns:
383 google.oauth2.credentials.Credentials: The constructed
384 credentials.
385
386 Raises:
387 ValueError: If the file is not in the expected format.
388 """
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700389 with io.open(filename, "r", encoding="utf-8") as json_file:
Hiranya Jayathilaka23c88f72017-12-05 09:29:59 -0800390 data = json.load(json_file)
391 return cls.from_authorized_user_info(data, scopes)
patkasperbfb1f8c2019-12-05 22:03:44 +0100392
393 def to_json(self, strip=None):
394 """Utility function that creates a JSON representation of a Credentials
395 object.
396
397 Args:
398 strip (Sequence[str]): Optional list of members to exclude from the
399 generated JSON.
400
401 Returns:
alvyjudy67d38d82020-06-10 22:23:31 -0400402 str: A JSON representation of this instance. When converted into
403 a dictionary, it can be passed to from_authorized_user_info()
404 to create a new credential instance.
patkasperbfb1f8c2019-12-05 22:03:44 +0100405 """
406 prep = {
407 "token": self.token,
408 "refresh_token": self.refresh_token,
409 "token_uri": self.token_uri,
410 "client_id": self.client_id,
411 "client_secret": self.client_secret,
412 "scopes": self.scopes,
arithmetic172882293fe2021-04-14 11:22:13 -0700413 "rapt_token": self.rapt_token,
patkasperbfb1f8c2019-12-05 22:03:44 +0100414 }
wesley chund0e0aba2020-09-17 09:18:55 -0700415 if self.expiry: # flatten expiry timestamp
416 prep["expiry"] = self.expiry.isoformat() + "Z"
patkasperbfb1f8c2019-12-05 22:03:44 +0100417
wesley chund0e0aba2020-09-17 09:18:55 -0700418 # Remove empty entries (those which are None)
patkasperbfb1f8c2019-12-05 22:03:44 +0100419 prep = {k: v for k, v in prep.items() if v is not None}
420
421 # Remove entries that explicitely need to be removed
422 if strip is not None:
423 prep = {k: v for k, v in prep.items() if k not in strip}
424
425 return json.dumps(prep)
arithmetic1728772dac62020-03-27 14:34:13 -0700426
427
Bu Sun Kim41599ae2020-09-02 12:55:42 -0600428class UserAccessTokenCredentials(credentials.CredentialsWithQuotaProject):
arithmetic1728772dac62020-03-27 14:34:13 -0700429 """Access token credentials for user account.
430
431 Obtain the access token for a given user account or the current active
432 user account with the ``gcloud auth print-access-token`` command.
433
434 Args:
435 account (Optional[str]): Account to get the access token for. If not
436 specified, the current active account will be used.
Bu Sun Kim3dda7b22020-07-09 10:39:39 -0700437 quota_project_id (Optional[str]): The project ID used for quota
438 and billing.
arithmetic1728772dac62020-03-27 14:34:13 -0700439 """
440
Bu Sun Kim3dda7b22020-07-09 10:39:39 -0700441 def __init__(self, account=None, quota_project_id=None):
arithmetic1728772dac62020-03-27 14:34:13 -0700442 super(UserAccessTokenCredentials, self).__init__()
443 self._account = account
Bu Sun Kim3dda7b22020-07-09 10:39:39 -0700444 self._quota_project_id = quota_project_id
arithmetic1728772dac62020-03-27 14:34:13 -0700445
446 def with_account(self, account):
447 """Create a new instance with the given account.
448
449 Args:
450 account (str): Account to get the access token for.
451
452 Returns:
453 google.oauth2.credentials.UserAccessTokenCredentials: The created
454 credentials with the given account.
455 """
Bu Sun Kim3dda7b22020-07-09 10:39:39 -0700456 return self.__class__(account=account, quota_project_id=self._quota_project_id)
457
Bu Sun Kim41599ae2020-09-02 12:55:42 -0600458 @_helpers.copy_docstring(credentials.CredentialsWithQuotaProject)
Bu Sun Kim3dda7b22020-07-09 10:39:39 -0700459 def with_quota_project(self, quota_project_id):
460 return self.__class__(account=self._account, quota_project_id=quota_project_id)
arithmetic1728772dac62020-03-27 14:34:13 -0700461
462 def refresh(self, request):
463 """Refreshes the access token.
464
465 Args:
466 request (google.auth.transport.Request): This argument is required
467 by the base class interface but not used in this implementation,
468 so just set it to `None`.
469
470 Raises:
471 google.auth.exceptions.UserAccessTokenError: If the access token
472 refresh failed.
473 """
474 self.token = _cloud_sdk.get_auth_access_token(self._account)
475
476 @_helpers.copy_docstring(credentials.Credentials)
477 def before_request(self, request, method, url, headers):
478 self.refresh(request)
479 self.apply(headers)