blob: dcfa5f91222dee01e4ee330ed4c62803a020b867 [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,
Bu Sun Kim9eec0912019-10-21 17:04:21 -070077 ):
Jon Wayne Parrott10ec7e92016-10-17 10:46:38 -070078 """
79 Args:
80 token (Optional(str)): The OAuth 2.0 access token. Can be None
81 if refresh information is provided.
82 refresh_token (str): The OAuth 2.0 refresh token. If specified,
83 credentials can be refreshed.
Jon Wayne Parrott26a16372017-03-28 13:03:33 -070084 id_token (str): The Open ID Connect ID Token.
Jon Wayne Parrott10ec7e92016-10-17 10:46:38 -070085 token_uri (str): The OAuth 2.0 authorization server's token
86 endpoint URI. Must be specified for refresh, can be left as
87 None if the token can not be refreshed.
88 client_id (str): The OAuth 2.0 client ID. Must be specified for
89 refresh, can be left as None if the token can not be refreshed.
90 client_secret(str): The OAuth 2.0 client secret. Must be specified
91 for refresh, can be left as None if the token can not be
92 refreshed.
Eugene W. Foley49a18c42019-05-22 13:50:38 -040093 scopes (Sequence[str]): The scopes used to obtain authorization.
94 This parameter is used by :meth:`has_scopes`. OAuth 2.0
95 credentials can not request additional scopes after
96 authorization. The scopes must be derivable from the refresh
97 token if refresh information is provided (e.g. The refresh
98 token scopes are a superset of this or contain a wild card
99 scope like 'https://www.googleapis.com/auth/any-api').
Bu Sun Kimbf5ce0c2021-02-01 15:17:49 -0700100 default_scopes (Sequence[str]): Default scopes passed by a
101 Google client library. Use 'scopes' for user-defined scopes.
Bu Sun Kim32d71a52019-12-18 11:30:46 -0800102 quota_project_id (Optional[str]): The project ID used for quota and billing.
103 This project may be different from the project used to
104 create the credentials.
arithmetic172882293fe2021-04-14 11:22:13 -0700105 rapt_token (Optional[str]): The reauth Proof Token.
Jon Wayne Parrott10ec7e92016-10-17 10:46:38 -0700106 """
107 super(Credentials, self).__init__()
108 self.token = token
wesley chund0e0aba2020-09-17 09:18:55 -0700109 self.expiry = expiry
Jon Wayne Parrott10ec7e92016-10-17 10:46:38 -0700110 self._refresh_token = refresh_token
Jon Wayne Parrott26a16372017-03-28 13:03:33 -0700111 self._id_token = id_token
Jon Wayne Parrott10ec7e92016-10-17 10:46:38 -0700112 self._scopes = scopes
Bu Sun Kimbf5ce0c2021-02-01 15:17:49 -0700113 self._default_scopes = default_scopes
Jon Wayne Parrott10ec7e92016-10-17 10:46:38 -0700114 self._token_uri = token_uri
115 self._client_id = client_id
116 self._client_secret = client_secret
Bu Sun Kim32d71a52019-12-18 11:30:46 -0800117 self._quota_project_id = quota_project_id
arithmetic172882293fe2021-04-14 11:22:13 -0700118 self._rapt_token = rapt_token
Bu Sun Kim32d71a52019-12-18 11:30:46 -0800119
120 def __getstate__(self):
121 """A __getstate__ method must exist for the __setstate__ to be called
122 This is identical to the default implementation.
123 See https://docs.python.org/3.7/library/pickle.html#object.__setstate__
124 """
125 return self.__dict__
126
127 def __setstate__(self, d):
128 """Credentials pickled with older versions of the class do not have
129 all the attributes."""
130 self.token = d.get("token")
131 self.expiry = d.get("expiry")
132 self._refresh_token = d.get("_refresh_token")
133 self._id_token = d.get("_id_token")
134 self._scopes = d.get("_scopes")
Bu Sun Kimbf5ce0c2021-02-01 15:17:49 -0700135 self._default_scopes = d.get("_default_scopes")
Bu Sun Kim32d71a52019-12-18 11:30:46 -0800136 self._token_uri = d.get("_token_uri")
137 self._client_id = d.get("_client_id")
138 self._client_secret = d.get("_client_secret")
139 self._quota_project_id = d.get("_quota_project_id")
arithmetic172882293fe2021-04-14 11:22:13 -0700140 self._rapt_token = d.get("_rapt_token")
Jon Wayne Parrott10ec7e92016-10-17 10:46:38 -0700141
142 @property
Jon Wayne Parrott2d0549a2017-03-01 09:27:16 -0800143 def refresh_token(self):
144 """Optional[str]: The OAuth 2.0 refresh token."""
145 return self._refresh_token
146
147 @property
wesley chund0e0aba2020-09-17 09:18:55 -0700148 def scopes(self):
149 """Optional[str]: The OAuth 2.0 permission scopes."""
150 return self._scopes
151
152 @property
Jon Wayne Parrott2d0549a2017-03-01 09:27:16 -0800153 def token_uri(self):
154 """Optional[str]: The OAuth 2.0 authorization server's token endpoint
155 URI."""
156 return self._token_uri
157
158 @property
Jon Wayne Parrott26a16372017-03-28 13:03:33 -0700159 def id_token(self):
160 """Optional[str]: The Open ID Connect ID Token.
161
162 Depending on the authorization server and the scopes requested, this
163 may be populated when credentials are obtained and updated when
164 :meth:`refresh` is called. This token is a JWT. It can be verified
165 and decoded using :func:`google.oauth2.id_token.verify_oauth2_token`.
166 """
167 return self._id_token
168
169 @property
Jon Wayne Parrott2d0549a2017-03-01 09:27:16 -0800170 def client_id(self):
171 """Optional[str]: The OAuth 2.0 client ID."""
172 return self._client_id
173
174 @property
175 def client_secret(self):
176 """Optional[str]: The OAuth 2.0 client secret."""
177 return self._client_secret
178
179 @property
Jon Wayne Parrott10ec7e92016-10-17 10:46:38 -0700180 def requires_scopes(self):
181 """False: OAuth 2.0 credentials have their scopes set when
182 the initial token is requested and can not be changed."""
183 return False
184
arithmetic172882293fe2021-04-14 11:22:13 -0700185 @property
186 def rapt_token(self):
187 """Optional[str]: The reauth Proof Token."""
188 return self._rapt_token
189
Bu Sun Kim41599ae2020-09-02 12:55:42 -0600190 @_helpers.copy_docstring(credentials.CredentialsWithQuotaProject)
Bu Sun Kimb12488c2020-06-10 13:44:07 -0700191 def with_quota_project(self, quota_project_id):
Bu Sun Kimb12488c2020-06-10 13:44:07 -0700192
Bu Sun Kimb12488c2020-06-10 13:44:07 -0700193 return self.__class__(
194 self.token,
195 refresh_token=self.refresh_token,
196 id_token=self.id_token,
197 token_uri=self.token_uri,
198 client_id=self.client_id,
199 client_secret=self.client_secret,
200 scopes=self.scopes,
Bu Sun Kimbf5ce0c2021-02-01 15:17:49 -0700201 default_scopes=self.default_scopes,
Bu Sun Kimb12488c2020-06-10 13:44:07 -0700202 quota_project_id=quota_project_id,
arithmetic172882293fe2021-04-14 11:22:13 -0700203 rapt_token=self.rapt_token,
Bu Sun Kimb12488c2020-06-10 13:44:07 -0700204 )
205
Jon Wayne Parrott10ec7e92016-10-17 10:46:38 -0700206 @_helpers.copy_docstring(credentials.Credentials)
207 def refresh(self, request):
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700208 if (
209 self._refresh_token is None
210 or self._token_uri is None
211 or self._client_id is None
212 or self._client_secret is None
213 ):
Thea Flowers118c0482018-05-24 13:34:07 -0700214 raise exceptions.RefreshError(
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700215 "The credentials do not contain the necessary fields need to "
216 "refresh the access token. You must specify refresh_token, "
217 "token_uri, client_id, and client_secret."
218 )
Thea Flowers118c0482018-05-24 13:34:07 -0700219
Bu Sun Kimbf5ce0c2021-02-01 15:17:49 -0700220 scopes = self._scopes if self._scopes is not None else self._default_scopes
221
arithmetic172882293fe2021-04-14 11:22:13 -0700222 (
223 access_token,
224 refresh_token,
225 expiry,
226 grant_response,
227 rapt_token,
228 ) = reauth.refresh_grant(
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700229 request,
230 self._token_uri,
231 self._refresh_token,
232 self._client_id,
233 self._client_secret,
arithmetic172882293fe2021-04-14 11:22:13 -0700234 scopes=scopes,
235 rapt_token=self._rapt_token,
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700236 )
Jon Wayne Parrott10ec7e92016-10-17 10:46:38 -0700237
238 self.token = access_token
239 self.expiry = expiry
240 self._refresh_token = refresh_token
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700241 self._id_token = grant_response.get("id_token")
arithmetic172882293fe2021-04-14 11:22:13 -0700242 self._rapt_token = rapt_token
Hiranya Jayathilaka23c88f72017-12-05 09:29:59 -0800243
arithmetic172882293fe2021-04-14 11:22:13 -0700244 if scopes and "scope" in grant_response:
Bu Sun Kimbf5ce0c2021-02-01 15:17:49 -0700245 requested_scopes = frozenset(scopes)
arithmetic172882293fe2021-04-14 11:22:13 -0700246 granted_scopes = frozenset(grant_response["scope"].split())
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700247 scopes_requested_but_not_granted = requested_scopes - granted_scopes
Eugene W. Foley49a18c42019-05-22 13:50:38 -0400248 if scopes_requested_but_not_granted:
249 raise exceptions.RefreshError(
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700250 "Not all requested scopes were granted by the "
251 "authorization server, missing scopes {}.".format(
252 ", ".join(scopes_requested_but_not_granted)
253 )
254 )
Eugene W. Foley49a18c42019-05-22 13:50:38 -0400255
Hiranya Jayathilaka23c88f72017-12-05 09:29:59 -0800256 @classmethod
257 def from_authorized_user_info(cls, info, scopes=None):
258 """Creates a Credentials instance from parsed authorized user info.
259
260 Args:
261 info (Mapping[str, str]): The authorized user info in Google
262 format.
263 scopes (Sequence[str]): Optional list of scopes to include in the
264 credentials.
265
266 Returns:
267 google.oauth2.credentials.Credentials: The constructed
268 credentials.
269
270 Raises:
271 ValueError: If the info is not in the expected format.
272 """
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700273 keys_needed = set(("refresh_token", "client_id", "client_secret"))
Hiranya Jayathilaka23c88f72017-12-05 09:29:59 -0800274 missing = keys_needed.difference(six.iterkeys(info))
275
276 if missing:
277 raise ValueError(
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700278 "Authorized user info was not in the expected format, missing "
279 "fields {}.".format(", ".join(missing))
280 )
Hiranya Jayathilaka23c88f72017-12-05 09:29:59 -0800281
wesley chund0e0aba2020-09-17 09:18:55 -0700282 # access token expiry (datetime obj); auto-expire if not saved
283 expiry = info.get("expiry")
284 if expiry:
285 expiry = datetime.strptime(
286 expiry.rstrip("Z").split(".")[0], "%Y-%m-%dT%H:%M:%S"
287 )
288 else:
289 expiry = _helpers.utcnow() - _helpers.CLOCK_SKEW
290
291 # process scopes, which needs to be a seq
292 if scopes is None and "scopes" in info:
293 scopes = info.get("scopes")
294 if isinstance(scopes, str):
295 scopes = scopes.split(" ")
296
Emile Caron530f5f92019-07-26 01:23:25 +0200297 return cls(
wesley chund0e0aba2020-09-17 09:18:55 -0700298 token=info.get("token"),
299 refresh_token=info.get("refresh_token"),
300 token_uri=_GOOGLE_OAUTH2_TOKEN_ENDPOINT, # always overrides
Hiranya Jayathilaka23c88f72017-12-05 09:29:59 -0800301 scopes=scopes,
wesley chund0e0aba2020-09-17 09:18:55 -0700302 client_id=info.get("client_id"),
303 client_secret=info.get("client_secret"),
304 quota_project_id=info.get("quota_project_id"), # may not exist
305 expiry=expiry,
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700306 )
Hiranya Jayathilaka23c88f72017-12-05 09:29:59 -0800307
308 @classmethod
309 def from_authorized_user_file(cls, filename, scopes=None):
310 """Creates a Credentials instance from an authorized user json file.
311
312 Args:
313 filename (str): The path to the authorized user json file.
314 scopes (Sequence[str]): Optional list of scopes to include in the
315 credentials.
316
317 Returns:
318 google.oauth2.credentials.Credentials: The constructed
319 credentials.
320
321 Raises:
322 ValueError: If the file is not in the expected format.
323 """
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700324 with io.open(filename, "r", encoding="utf-8") as json_file:
Hiranya Jayathilaka23c88f72017-12-05 09:29:59 -0800325 data = json.load(json_file)
326 return cls.from_authorized_user_info(data, scopes)
patkasperbfb1f8c2019-12-05 22:03:44 +0100327
328 def to_json(self, strip=None):
329 """Utility function that creates a JSON representation of a Credentials
330 object.
331
332 Args:
333 strip (Sequence[str]): Optional list of members to exclude from the
334 generated JSON.
335
336 Returns:
alvyjudy67d38d82020-06-10 22:23:31 -0400337 str: A JSON representation of this instance. When converted into
338 a dictionary, it can be passed to from_authorized_user_info()
339 to create a new credential instance.
patkasperbfb1f8c2019-12-05 22:03:44 +0100340 """
341 prep = {
342 "token": self.token,
343 "refresh_token": self.refresh_token,
344 "token_uri": self.token_uri,
345 "client_id": self.client_id,
346 "client_secret": self.client_secret,
347 "scopes": self.scopes,
arithmetic172882293fe2021-04-14 11:22:13 -0700348 "rapt_token": self.rapt_token,
patkasperbfb1f8c2019-12-05 22:03:44 +0100349 }
wesley chund0e0aba2020-09-17 09:18:55 -0700350 if self.expiry: # flatten expiry timestamp
351 prep["expiry"] = self.expiry.isoformat() + "Z"
patkasperbfb1f8c2019-12-05 22:03:44 +0100352
wesley chund0e0aba2020-09-17 09:18:55 -0700353 # Remove empty entries (those which are None)
patkasperbfb1f8c2019-12-05 22:03:44 +0100354 prep = {k: v for k, v in prep.items() if v is not None}
355
356 # Remove entries that explicitely need to be removed
357 if strip is not None:
358 prep = {k: v for k, v in prep.items() if k not in strip}
359
360 return json.dumps(prep)
arithmetic1728772dac62020-03-27 14:34:13 -0700361
362
Bu Sun Kim41599ae2020-09-02 12:55:42 -0600363class UserAccessTokenCredentials(credentials.CredentialsWithQuotaProject):
arithmetic1728772dac62020-03-27 14:34:13 -0700364 """Access token credentials for user account.
365
366 Obtain the access token for a given user account or the current active
367 user account with the ``gcloud auth print-access-token`` command.
368
369 Args:
370 account (Optional[str]): Account to get the access token for. If not
371 specified, the current active account will be used.
Bu Sun Kim3dda7b22020-07-09 10:39:39 -0700372 quota_project_id (Optional[str]): The project ID used for quota
373 and billing.
arithmetic1728772dac62020-03-27 14:34:13 -0700374 """
375
Bu Sun Kim3dda7b22020-07-09 10:39:39 -0700376 def __init__(self, account=None, quota_project_id=None):
arithmetic1728772dac62020-03-27 14:34:13 -0700377 super(UserAccessTokenCredentials, self).__init__()
378 self._account = account
Bu Sun Kim3dda7b22020-07-09 10:39:39 -0700379 self._quota_project_id = quota_project_id
arithmetic1728772dac62020-03-27 14:34:13 -0700380
381 def with_account(self, account):
382 """Create a new instance with the given account.
383
384 Args:
385 account (str): Account to get the access token for.
386
387 Returns:
388 google.oauth2.credentials.UserAccessTokenCredentials: The created
389 credentials with the given account.
390 """
Bu Sun Kim3dda7b22020-07-09 10:39:39 -0700391 return self.__class__(account=account, quota_project_id=self._quota_project_id)
392
Bu Sun Kim41599ae2020-09-02 12:55:42 -0600393 @_helpers.copy_docstring(credentials.CredentialsWithQuotaProject)
Bu Sun Kim3dda7b22020-07-09 10:39:39 -0700394 def with_quota_project(self, quota_project_id):
395 return self.__class__(account=self._account, quota_project_id=quota_project_id)
arithmetic1728772dac62020-03-27 14:34:13 -0700396
397 def refresh(self, request):
398 """Refreshes the access token.
399
400 Args:
401 request (google.auth.transport.Request): This argument is required
402 by the base class interface but not used in this implementation,
403 so just set it to `None`.
404
405 Raises:
406 google.auth.exceptions.UserAccessTokenError: If the access token
407 refresh failed.
408 """
409 self.token = _cloud_sdk.get_auth_access_token(self._account)
410
411 @_helpers.copy_docstring(credentials.Credentials)
412 def before_request(self, request, method, url, headers):
413 self.refresh(request)
414 self.apply(headers)