blob: 464cc4878ca678bf81d8241c53083a973a6c10d9 [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
Jon Wayne Parrott10ec7e92016-10-17 10:46:38 -070044from google.oauth2 import _client
45
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)
58 """
Jon Wayne Parrott10ec7e92016-10-17 10:46:38 -070059
Bu Sun Kim9eec0912019-10-21 17:04:21 -070060 def __init__(
61 self,
62 token,
63 refresh_token=None,
64 id_token=None,
65 token_uri=None,
66 client_id=None,
67 client_secret=None,
68 scopes=None,
Bu Sun Kimbf5ce0c2021-02-01 15:17:49 -070069 default_scopes=None,
Bu Sun Kim32d71a52019-12-18 11:30:46 -080070 quota_project_id=None,
wesley chund0e0aba2020-09-17 09:18:55 -070071 expiry=None,
Bu Sun Kim9eec0912019-10-21 17:04:21 -070072 ):
Jon Wayne Parrott10ec7e92016-10-17 10:46:38 -070073 """
74 Args:
75 token (Optional(str)): The OAuth 2.0 access token. Can be None
76 if refresh information is provided.
77 refresh_token (str): The OAuth 2.0 refresh token. If specified,
78 credentials can be refreshed.
Jon Wayne Parrott26a16372017-03-28 13:03:33 -070079 id_token (str): The Open ID Connect ID Token.
Jon Wayne Parrott10ec7e92016-10-17 10:46:38 -070080 token_uri (str): The OAuth 2.0 authorization server's token
81 endpoint URI. Must be specified for refresh, can be left as
82 None if the token can not be refreshed.
83 client_id (str): The OAuth 2.0 client ID. Must be specified for
84 refresh, can be left as None if the token can not be refreshed.
85 client_secret(str): The OAuth 2.0 client secret. Must be specified
86 for refresh, can be left as None if the token can not be
87 refreshed.
Eugene W. Foley49a18c42019-05-22 13:50:38 -040088 scopes (Sequence[str]): The scopes used to obtain authorization.
89 This parameter is used by :meth:`has_scopes`. OAuth 2.0
90 credentials can not request additional scopes after
91 authorization. The scopes must be derivable from the refresh
92 token if refresh information is provided (e.g. The refresh
93 token scopes are a superset of this or contain a wild card
94 scope like 'https://www.googleapis.com/auth/any-api').
Bu Sun Kimbf5ce0c2021-02-01 15:17:49 -070095 default_scopes (Sequence[str]): Default scopes passed by a
96 Google client library. Use 'scopes' for user-defined scopes.
Bu Sun Kim32d71a52019-12-18 11:30:46 -080097 quota_project_id (Optional[str]): The project ID used for quota and billing.
98 This project may be different from the project used to
99 create the credentials.
Jon Wayne Parrott10ec7e92016-10-17 10:46:38 -0700100 """
101 super(Credentials, self).__init__()
102 self.token = token
wesley chund0e0aba2020-09-17 09:18:55 -0700103 self.expiry = expiry
Jon Wayne Parrott10ec7e92016-10-17 10:46:38 -0700104 self._refresh_token = refresh_token
Jon Wayne Parrott26a16372017-03-28 13:03:33 -0700105 self._id_token = id_token
Jon Wayne Parrott10ec7e92016-10-17 10:46:38 -0700106 self._scopes = scopes
Bu Sun Kimbf5ce0c2021-02-01 15:17:49 -0700107 self._default_scopes = default_scopes
Jon Wayne Parrott10ec7e92016-10-17 10:46:38 -0700108 self._token_uri = token_uri
109 self._client_id = client_id
110 self._client_secret = client_secret
Bu Sun Kim32d71a52019-12-18 11:30:46 -0800111 self._quota_project_id = quota_project_id
112
113 def __getstate__(self):
114 """A __getstate__ method must exist for the __setstate__ to be called
115 This is identical to the default implementation.
116 See https://docs.python.org/3.7/library/pickle.html#object.__setstate__
117 """
118 return self.__dict__
119
120 def __setstate__(self, d):
121 """Credentials pickled with older versions of the class do not have
122 all the attributes."""
123 self.token = d.get("token")
124 self.expiry = d.get("expiry")
125 self._refresh_token = d.get("_refresh_token")
126 self._id_token = d.get("_id_token")
127 self._scopes = d.get("_scopes")
Bu Sun Kimbf5ce0c2021-02-01 15:17:49 -0700128 self._default_scopes = d.get("_default_scopes")
Bu Sun Kim32d71a52019-12-18 11:30:46 -0800129 self._token_uri = d.get("_token_uri")
130 self._client_id = d.get("_client_id")
131 self._client_secret = d.get("_client_secret")
132 self._quota_project_id = d.get("_quota_project_id")
Jon Wayne Parrott10ec7e92016-10-17 10:46:38 -0700133
134 @property
Jon Wayne Parrott2d0549a2017-03-01 09:27:16 -0800135 def refresh_token(self):
136 """Optional[str]: The OAuth 2.0 refresh token."""
137 return self._refresh_token
138
139 @property
wesley chund0e0aba2020-09-17 09:18:55 -0700140 def scopes(self):
141 """Optional[str]: The OAuth 2.0 permission scopes."""
142 return self._scopes
143
144 @property
Jon Wayne Parrott2d0549a2017-03-01 09:27:16 -0800145 def token_uri(self):
146 """Optional[str]: The OAuth 2.0 authorization server's token endpoint
147 URI."""
148 return self._token_uri
149
150 @property
Jon Wayne Parrott26a16372017-03-28 13:03:33 -0700151 def id_token(self):
152 """Optional[str]: The Open ID Connect ID Token.
153
154 Depending on the authorization server and the scopes requested, this
155 may be populated when credentials are obtained and updated when
156 :meth:`refresh` is called. This token is a JWT. It can be verified
157 and decoded using :func:`google.oauth2.id_token.verify_oauth2_token`.
158 """
159 return self._id_token
160
161 @property
Jon Wayne Parrott2d0549a2017-03-01 09:27:16 -0800162 def client_id(self):
163 """Optional[str]: The OAuth 2.0 client ID."""
164 return self._client_id
165
166 @property
167 def client_secret(self):
168 """Optional[str]: The OAuth 2.0 client secret."""
169 return self._client_secret
170
171 @property
Jon Wayne Parrott10ec7e92016-10-17 10:46:38 -0700172 def requires_scopes(self):
173 """False: OAuth 2.0 credentials have their scopes set when
174 the initial token is requested and can not be changed."""
175 return False
176
Bu Sun Kim41599ae2020-09-02 12:55:42 -0600177 @_helpers.copy_docstring(credentials.CredentialsWithQuotaProject)
Bu Sun Kimb12488c2020-06-10 13:44:07 -0700178 def with_quota_project(self, quota_project_id):
Bu Sun Kimb12488c2020-06-10 13:44:07 -0700179
Bu Sun Kimb12488c2020-06-10 13:44:07 -0700180 return self.__class__(
181 self.token,
182 refresh_token=self.refresh_token,
183 id_token=self.id_token,
184 token_uri=self.token_uri,
185 client_id=self.client_id,
186 client_secret=self.client_secret,
187 scopes=self.scopes,
Bu Sun Kimbf5ce0c2021-02-01 15:17:49 -0700188 default_scopes=self.default_scopes,
Bu Sun Kimb12488c2020-06-10 13:44:07 -0700189 quota_project_id=quota_project_id,
190 )
191
Jon Wayne Parrott10ec7e92016-10-17 10:46:38 -0700192 @_helpers.copy_docstring(credentials.Credentials)
193 def refresh(self, request):
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700194 if (
195 self._refresh_token is None
196 or self._token_uri is None
197 or self._client_id is None
198 or self._client_secret is None
199 ):
Thea Flowers118c0482018-05-24 13:34:07 -0700200 raise exceptions.RefreshError(
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700201 "The credentials do not contain the necessary fields need to "
202 "refresh the access token. You must specify refresh_token, "
203 "token_uri, client_id, and client_secret."
204 )
Thea Flowers118c0482018-05-24 13:34:07 -0700205
Bu Sun Kimbf5ce0c2021-02-01 15:17:49 -0700206 scopes = self._scopes if self._scopes is not None else self._default_scopes
207
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700208 access_token, refresh_token, expiry, grant_response = _client.refresh_grant(
209 request,
210 self._token_uri,
211 self._refresh_token,
212 self._client_id,
213 self._client_secret,
Bu Sun Kimbf5ce0c2021-02-01 15:17:49 -0700214 scopes,
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700215 )
Jon Wayne Parrott10ec7e92016-10-17 10:46:38 -0700216
217 self.token = access_token
218 self.expiry = expiry
219 self._refresh_token = refresh_token
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700220 self._id_token = grant_response.get("id_token")
Hiranya Jayathilaka23c88f72017-12-05 09:29:59 -0800221
Bu Sun Kimbf5ce0c2021-02-01 15:17:49 -0700222 if scopes and "scopes" in grant_response:
223 requested_scopes = frozenset(scopes)
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700224 granted_scopes = frozenset(grant_response["scopes"].split())
225 scopes_requested_but_not_granted = requested_scopes - granted_scopes
Eugene W. Foley49a18c42019-05-22 13:50:38 -0400226 if scopes_requested_but_not_granted:
227 raise exceptions.RefreshError(
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700228 "Not all requested scopes were granted by the "
229 "authorization server, missing scopes {}.".format(
230 ", ".join(scopes_requested_but_not_granted)
231 )
232 )
Eugene W. Foley49a18c42019-05-22 13:50:38 -0400233
Hiranya Jayathilaka23c88f72017-12-05 09:29:59 -0800234 @classmethod
235 def from_authorized_user_info(cls, info, scopes=None):
236 """Creates a Credentials instance from parsed authorized user info.
237
238 Args:
239 info (Mapping[str, str]): The authorized user info in Google
240 format.
241 scopes (Sequence[str]): Optional list of scopes to include in the
242 credentials.
243
244 Returns:
245 google.oauth2.credentials.Credentials: The constructed
246 credentials.
247
248 Raises:
249 ValueError: If the info is not in the expected format.
250 """
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700251 keys_needed = set(("refresh_token", "client_id", "client_secret"))
Hiranya Jayathilaka23c88f72017-12-05 09:29:59 -0800252 missing = keys_needed.difference(six.iterkeys(info))
253
254 if missing:
255 raise ValueError(
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700256 "Authorized user info was not in the expected format, missing "
257 "fields {}.".format(", ".join(missing))
258 )
Hiranya Jayathilaka23c88f72017-12-05 09:29:59 -0800259
wesley chund0e0aba2020-09-17 09:18:55 -0700260 # access token expiry (datetime obj); auto-expire if not saved
261 expiry = info.get("expiry")
262 if expiry:
263 expiry = datetime.strptime(
264 expiry.rstrip("Z").split(".")[0], "%Y-%m-%dT%H:%M:%S"
265 )
266 else:
267 expiry = _helpers.utcnow() - _helpers.CLOCK_SKEW
268
269 # process scopes, which needs to be a seq
270 if scopes is None and "scopes" in info:
271 scopes = info.get("scopes")
272 if isinstance(scopes, str):
273 scopes = scopes.split(" ")
274
Emile Caron530f5f92019-07-26 01:23:25 +0200275 return cls(
wesley chund0e0aba2020-09-17 09:18:55 -0700276 token=info.get("token"),
277 refresh_token=info.get("refresh_token"),
278 token_uri=_GOOGLE_OAUTH2_TOKEN_ENDPOINT, # always overrides
Hiranya Jayathilaka23c88f72017-12-05 09:29:59 -0800279 scopes=scopes,
wesley chund0e0aba2020-09-17 09:18:55 -0700280 client_id=info.get("client_id"),
281 client_secret=info.get("client_secret"),
282 quota_project_id=info.get("quota_project_id"), # may not exist
283 expiry=expiry,
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700284 )
Hiranya Jayathilaka23c88f72017-12-05 09:29:59 -0800285
286 @classmethod
287 def from_authorized_user_file(cls, filename, scopes=None):
288 """Creates a Credentials instance from an authorized user json file.
289
290 Args:
291 filename (str): The path to the authorized user json file.
292 scopes (Sequence[str]): Optional list of scopes to include in the
293 credentials.
294
295 Returns:
296 google.oauth2.credentials.Credentials: The constructed
297 credentials.
298
299 Raises:
300 ValueError: If the file is not in the expected format.
301 """
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700302 with io.open(filename, "r", encoding="utf-8") as json_file:
Hiranya Jayathilaka23c88f72017-12-05 09:29:59 -0800303 data = json.load(json_file)
304 return cls.from_authorized_user_info(data, scopes)
patkasperbfb1f8c2019-12-05 22:03:44 +0100305
306 def to_json(self, strip=None):
307 """Utility function that creates a JSON representation of a Credentials
308 object.
309
310 Args:
311 strip (Sequence[str]): Optional list of members to exclude from the
312 generated JSON.
313
314 Returns:
alvyjudy67d38d82020-06-10 22:23:31 -0400315 str: A JSON representation of this instance. When converted into
316 a dictionary, it can be passed to from_authorized_user_info()
317 to create a new credential instance.
patkasperbfb1f8c2019-12-05 22:03:44 +0100318 """
319 prep = {
320 "token": self.token,
321 "refresh_token": self.refresh_token,
322 "token_uri": self.token_uri,
323 "client_id": self.client_id,
324 "client_secret": self.client_secret,
325 "scopes": self.scopes,
326 }
wesley chund0e0aba2020-09-17 09:18:55 -0700327 if self.expiry: # flatten expiry timestamp
328 prep["expiry"] = self.expiry.isoformat() + "Z"
patkasperbfb1f8c2019-12-05 22:03:44 +0100329
wesley chund0e0aba2020-09-17 09:18:55 -0700330 # Remove empty entries (those which are None)
patkasperbfb1f8c2019-12-05 22:03:44 +0100331 prep = {k: v for k, v in prep.items() if v is not None}
332
333 # Remove entries that explicitely need to be removed
334 if strip is not None:
335 prep = {k: v for k, v in prep.items() if k not in strip}
336
337 return json.dumps(prep)
arithmetic1728772dac62020-03-27 14:34:13 -0700338
339
Bu Sun Kim41599ae2020-09-02 12:55:42 -0600340class UserAccessTokenCredentials(credentials.CredentialsWithQuotaProject):
arithmetic1728772dac62020-03-27 14:34:13 -0700341 """Access token credentials for user account.
342
343 Obtain the access token for a given user account or the current active
344 user account with the ``gcloud auth print-access-token`` command.
345
346 Args:
347 account (Optional[str]): Account to get the access token for. If not
348 specified, the current active account will be used.
Bu Sun Kim3dda7b22020-07-09 10:39:39 -0700349 quota_project_id (Optional[str]): The project ID used for quota
350 and billing.
arithmetic1728772dac62020-03-27 14:34:13 -0700351 """
352
Bu Sun Kim3dda7b22020-07-09 10:39:39 -0700353 def __init__(self, account=None, quota_project_id=None):
arithmetic1728772dac62020-03-27 14:34:13 -0700354 super(UserAccessTokenCredentials, self).__init__()
355 self._account = account
Bu Sun Kim3dda7b22020-07-09 10:39:39 -0700356 self._quota_project_id = quota_project_id
arithmetic1728772dac62020-03-27 14:34:13 -0700357
358 def with_account(self, account):
359 """Create a new instance with the given account.
360
361 Args:
362 account (str): Account to get the access token for.
363
364 Returns:
365 google.oauth2.credentials.UserAccessTokenCredentials: The created
366 credentials with the given account.
367 """
Bu Sun Kim3dda7b22020-07-09 10:39:39 -0700368 return self.__class__(account=account, quota_project_id=self._quota_project_id)
369
Bu Sun Kim41599ae2020-09-02 12:55:42 -0600370 @_helpers.copy_docstring(credentials.CredentialsWithQuotaProject)
Bu Sun Kim3dda7b22020-07-09 10:39:39 -0700371 def with_quota_project(self, quota_project_id):
372 return self.__class__(account=self._account, quota_project_id=quota_project_id)
arithmetic1728772dac62020-03-27 14:34:13 -0700373
374 def refresh(self, request):
375 """Refreshes the access token.
376
377 Args:
378 request (google.auth.transport.Request): This argument is required
379 by the base class interface but not used in this implementation,
380 so just set it to `None`.
381
382 Raises:
383 google.auth.exceptions.UserAccessTokenError: If the access token
384 refresh failed.
385 """
386 self.token = _cloud_sdk.get_auth_access_token(self._account)
387
388 @_helpers.copy_docstring(credentials.Credentials)
389 def before_request(self, request, method, url, headers):
390 self.refresh(request)
391 self.apply(headers)