blob: 36b8f0cb76240e4fd8139beb7ca35d06b0314644 [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 Kim32d71a52019-12-18 11:30:46 -080069 quota_project_id=None,
wesley chund0e0aba2020-09-17 09:18:55 -070070 expiry=None,
Bu Sun Kim9eec0912019-10-21 17:04:21 -070071 ):
Jon Wayne Parrott10ec7e92016-10-17 10:46:38 -070072 """
73 Args:
74 token (Optional(str)): The OAuth 2.0 access token. Can be None
75 if refresh information is provided.
76 refresh_token (str): The OAuth 2.0 refresh token. If specified,
77 credentials can be refreshed.
Jon Wayne Parrott26a16372017-03-28 13:03:33 -070078 id_token (str): The Open ID Connect ID Token.
Jon Wayne Parrott10ec7e92016-10-17 10:46:38 -070079 token_uri (str): The OAuth 2.0 authorization server's token
80 endpoint URI. Must be specified for refresh, can be left as
81 None if the token can not be refreshed.
82 client_id (str): The OAuth 2.0 client ID. Must be specified for
83 refresh, can be left as None if the token can not be refreshed.
84 client_secret(str): The OAuth 2.0 client secret. Must be specified
85 for refresh, can be left as None if the token can not be
86 refreshed.
Eugene W. Foley49a18c42019-05-22 13:50:38 -040087 scopes (Sequence[str]): The scopes used to obtain authorization.
88 This parameter is used by :meth:`has_scopes`. OAuth 2.0
89 credentials can not request additional scopes after
90 authorization. The scopes must be derivable from the refresh
91 token if refresh information is provided (e.g. The refresh
92 token scopes are a superset of this or contain a wild card
93 scope like 'https://www.googleapis.com/auth/any-api').
Bu Sun Kim32d71a52019-12-18 11:30:46 -080094 quota_project_id (Optional[str]): The project ID used for quota and billing.
95 This project may be different from the project used to
96 create the credentials.
Jon Wayne Parrott10ec7e92016-10-17 10:46:38 -070097 """
98 super(Credentials, self).__init__()
99 self.token = token
wesley chund0e0aba2020-09-17 09:18:55 -0700100 self.expiry = expiry
Jon Wayne Parrott10ec7e92016-10-17 10:46:38 -0700101 self._refresh_token = refresh_token
Jon Wayne Parrott26a16372017-03-28 13:03:33 -0700102 self._id_token = id_token
Jon Wayne Parrott10ec7e92016-10-17 10:46:38 -0700103 self._scopes = scopes
104 self._token_uri = token_uri
105 self._client_id = client_id
106 self._client_secret = client_secret
Bu Sun Kim32d71a52019-12-18 11:30:46 -0800107 self._quota_project_id = quota_project_id
108
109 def __getstate__(self):
110 """A __getstate__ method must exist for the __setstate__ to be called
111 This is identical to the default implementation.
112 See https://docs.python.org/3.7/library/pickle.html#object.__setstate__
113 """
114 return self.__dict__
115
116 def __setstate__(self, d):
117 """Credentials pickled with older versions of the class do not have
118 all the attributes."""
119 self.token = d.get("token")
120 self.expiry = d.get("expiry")
121 self._refresh_token = d.get("_refresh_token")
122 self._id_token = d.get("_id_token")
123 self._scopes = d.get("_scopes")
124 self._token_uri = d.get("_token_uri")
125 self._client_id = d.get("_client_id")
126 self._client_secret = d.get("_client_secret")
127 self._quota_project_id = d.get("_quota_project_id")
Jon Wayne Parrott10ec7e92016-10-17 10:46:38 -0700128
129 @property
Jon Wayne Parrott2d0549a2017-03-01 09:27:16 -0800130 def refresh_token(self):
131 """Optional[str]: The OAuth 2.0 refresh token."""
132 return self._refresh_token
133
134 @property
wesley chund0e0aba2020-09-17 09:18:55 -0700135 def scopes(self):
136 """Optional[str]: The OAuth 2.0 permission scopes."""
137 return self._scopes
138
139 @property
Jon Wayne Parrott2d0549a2017-03-01 09:27:16 -0800140 def token_uri(self):
141 """Optional[str]: The OAuth 2.0 authorization server's token endpoint
142 URI."""
143 return self._token_uri
144
145 @property
Jon Wayne Parrott26a16372017-03-28 13:03:33 -0700146 def id_token(self):
147 """Optional[str]: The Open ID Connect ID Token.
148
149 Depending on the authorization server and the scopes requested, this
150 may be populated when credentials are obtained and updated when
151 :meth:`refresh` is called. This token is a JWT. It can be verified
152 and decoded using :func:`google.oauth2.id_token.verify_oauth2_token`.
153 """
154 return self._id_token
155
156 @property
Jon Wayne Parrott2d0549a2017-03-01 09:27:16 -0800157 def client_id(self):
158 """Optional[str]: The OAuth 2.0 client ID."""
159 return self._client_id
160
161 @property
162 def client_secret(self):
163 """Optional[str]: The OAuth 2.0 client secret."""
164 return self._client_secret
165
166 @property
Jon Wayne Parrott10ec7e92016-10-17 10:46:38 -0700167 def requires_scopes(self):
168 """False: OAuth 2.0 credentials have their scopes set when
169 the initial token is requested and can not be changed."""
170 return False
171
Bu Sun Kim41599ae2020-09-02 12:55:42 -0600172 @_helpers.copy_docstring(credentials.CredentialsWithQuotaProject)
Bu Sun Kimb12488c2020-06-10 13:44:07 -0700173 def with_quota_project(self, quota_project_id):
Bu Sun Kimb12488c2020-06-10 13:44:07 -0700174
Bu Sun Kimb12488c2020-06-10 13:44:07 -0700175 return self.__class__(
176 self.token,
177 refresh_token=self.refresh_token,
178 id_token=self.id_token,
179 token_uri=self.token_uri,
180 client_id=self.client_id,
181 client_secret=self.client_secret,
182 scopes=self.scopes,
183 quota_project_id=quota_project_id,
184 )
185
Jon Wayne Parrott10ec7e92016-10-17 10:46:38 -0700186 @_helpers.copy_docstring(credentials.Credentials)
187 def refresh(self, request):
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700188 if (
189 self._refresh_token is None
190 or self._token_uri is None
191 or self._client_id is None
192 or self._client_secret is None
193 ):
Thea Flowers118c0482018-05-24 13:34:07 -0700194 raise exceptions.RefreshError(
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700195 "The credentials do not contain the necessary fields need to "
196 "refresh the access token. You must specify refresh_token, "
197 "token_uri, client_id, and client_secret."
198 )
Thea Flowers118c0482018-05-24 13:34:07 -0700199
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700200 access_token, refresh_token, expiry, grant_response = _client.refresh_grant(
201 request,
202 self._token_uri,
203 self._refresh_token,
204 self._client_id,
205 self._client_secret,
206 self._scopes,
207 )
Jon Wayne Parrott10ec7e92016-10-17 10:46:38 -0700208
209 self.token = access_token
210 self.expiry = expiry
211 self._refresh_token = refresh_token
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700212 self._id_token = grant_response.get("id_token")
Hiranya Jayathilaka23c88f72017-12-05 09:29:59 -0800213
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700214 if self._scopes and "scopes" in grant_response:
Eugene W. Foley49a18c42019-05-22 13:50:38 -0400215 requested_scopes = frozenset(self._scopes)
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700216 granted_scopes = frozenset(grant_response["scopes"].split())
217 scopes_requested_but_not_granted = requested_scopes - granted_scopes
Eugene W. Foley49a18c42019-05-22 13:50:38 -0400218 if scopes_requested_but_not_granted:
219 raise exceptions.RefreshError(
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700220 "Not all requested scopes were granted by the "
221 "authorization server, missing scopes {}.".format(
222 ", ".join(scopes_requested_but_not_granted)
223 )
224 )
Eugene W. Foley49a18c42019-05-22 13:50:38 -0400225
Hiranya Jayathilaka23c88f72017-12-05 09:29:59 -0800226 @classmethod
227 def from_authorized_user_info(cls, info, scopes=None):
228 """Creates a Credentials instance from parsed authorized user info.
229
230 Args:
231 info (Mapping[str, str]): The authorized user info in Google
232 format.
233 scopes (Sequence[str]): Optional list of scopes to include in the
234 credentials.
235
236 Returns:
237 google.oauth2.credentials.Credentials: The constructed
238 credentials.
239
240 Raises:
241 ValueError: If the info is not in the expected format.
242 """
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700243 keys_needed = set(("refresh_token", "client_id", "client_secret"))
Hiranya Jayathilaka23c88f72017-12-05 09:29:59 -0800244 missing = keys_needed.difference(six.iterkeys(info))
245
246 if missing:
247 raise ValueError(
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700248 "Authorized user info was not in the expected format, missing "
249 "fields {}.".format(", ".join(missing))
250 )
Hiranya Jayathilaka23c88f72017-12-05 09:29:59 -0800251
wesley chund0e0aba2020-09-17 09:18:55 -0700252 # access token expiry (datetime obj); auto-expire if not saved
253 expiry = info.get("expiry")
254 if expiry:
255 expiry = datetime.strptime(
256 expiry.rstrip("Z").split(".")[0], "%Y-%m-%dT%H:%M:%S"
257 )
258 else:
259 expiry = _helpers.utcnow() - _helpers.CLOCK_SKEW
260
261 # process scopes, which needs to be a seq
262 if scopes is None and "scopes" in info:
263 scopes = info.get("scopes")
264 if isinstance(scopes, str):
265 scopes = scopes.split(" ")
266
Emile Caron530f5f92019-07-26 01:23:25 +0200267 return cls(
wesley chund0e0aba2020-09-17 09:18:55 -0700268 token=info.get("token"),
269 refresh_token=info.get("refresh_token"),
270 token_uri=_GOOGLE_OAUTH2_TOKEN_ENDPOINT, # always overrides
Hiranya Jayathilaka23c88f72017-12-05 09:29:59 -0800271 scopes=scopes,
wesley chund0e0aba2020-09-17 09:18:55 -0700272 client_id=info.get("client_id"),
273 client_secret=info.get("client_secret"),
274 quota_project_id=info.get("quota_project_id"), # may not exist
275 expiry=expiry,
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700276 )
Hiranya Jayathilaka23c88f72017-12-05 09:29:59 -0800277
278 @classmethod
279 def from_authorized_user_file(cls, filename, scopes=None):
280 """Creates a Credentials instance from an authorized user json file.
281
282 Args:
283 filename (str): The path to the authorized user json file.
284 scopes (Sequence[str]): Optional list of scopes to include in the
285 credentials.
286
287 Returns:
288 google.oauth2.credentials.Credentials: The constructed
289 credentials.
290
291 Raises:
292 ValueError: If the file is not in the expected format.
293 """
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700294 with io.open(filename, "r", encoding="utf-8") as json_file:
Hiranya Jayathilaka23c88f72017-12-05 09:29:59 -0800295 data = json.load(json_file)
296 return cls.from_authorized_user_info(data, scopes)
patkasperbfb1f8c2019-12-05 22:03:44 +0100297
298 def to_json(self, strip=None):
299 """Utility function that creates a JSON representation of a Credentials
300 object.
301
302 Args:
303 strip (Sequence[str]): Optional list of members to exclude from the
304 generated JSON.
305
306 Returns:
alvyjudy67d38d82020-06-10 22:23:31 -0400307 str: A JSON representation of this instance. When converted into
308 a dictionary, it can be passed to from_authorized_user_info()
309 to create a new credential instance.
patkasperbfb1f8c2019-12-05 22:03:44 +0100310 """
311 prep = {
312 "token": self.token,
313 "refresh_token": self.refresh_token,
314 "token_uri": self.token_uri,
315 "client_id": self.client_id,
316 "client_secret": self.client_secret,
317 "scopes": self.scopes,
318 }
wesley chund0e0aba2020-09-17 09:18:55 -0700319 if self.expiry: # flatten expiry timestamp
320 prep["expiry"] = self.expiry.isoformat() + "Z"
patkasperbfb1f8c2019-12-05 22:03:44 +0100321
wesley chund0e0aba2020-09-17 09:18:55 -0700322 # Remove empty entries (those which are None)
patkasperbfb1f8c2019-12-05 22:03:44 +0100323 prep = {k: v for k, v in prep.items() if v is not None}
324
325 # Remove entries that explicitely need to be removed
326 if strip is not None:
327 prep = {k: v for k, v in prep.items() if k not in strip}
328
329 return json.dumps(prep)
arithmetic1728772dac62020-03-27 14:34:13 -0700330
331
Bu Sun Kim41599ae2020-09-02 12:55:42 -0600332class UserAccessTokenCredentials(credentials.CredentialsWithQuotaProject):
arithmetic1728772dac62020-03-27 14:34:13 -0700333 """Access token credentials for user account.
334
335 Obtain the access token for a given user account or the current active
336 user account with the ``gcloud auth print-access-token`` command.
337
338 Args:
339 account (Optional[str]): Account to get the access token for. If not
340 specified, the current active account will be used.
Bu Sun Kim3dda7b22020-07-09 10:39:39 -0700341 quota_project_id (Optional[str]): The project ID used for quota
342 and billing.
arithmetic1728772dac62020-03-27 14:34:13 -0700343 """
344
Bu Sun Kim3dda7b22020-07-09 10:39:39 -0700345 def __init__(self, account=None, quota_project_id=None):
arithmetic1728772dac62020-03-27 14:34:13 -0700346 super(UserAccessTokenCredentials, self).__init__()
347 self._account = account
Bu Sun Kim3dda7b22020-07-09 10:39:39 -0700348 self._quota_project_id = quota_project_id
arithmetic1728772dac62020-03-27 14:34:13 -0700349
350 def with_account(self, account):
351 """Create a new instance with the given account.
352
353 Args:
354 account (str): Account to get the access token for.
355
356 Returns:
357 google.oauth2.credentials.UserAccessTokenCredentials: The created
358 credentials with the given account.
359 """
Bu Sun Kim3dda7b22020-07-09 10:39:39 -0700360 return self.__class__(account=account, quota_project_id=self._quota_project_id)
361
Bu Sun Kim41599ae2020-09-02 12:55:42 -0600362 @_helpers.copy_docstring(credentials.CredentialsWithQuotaProject)
Bu Sun Kim3dda7b22020-07-09 10:39:39 -0700363 def with_quota_project(self, quota_project_id):
364 return self.__class__(account=self._account, quota_project_id=quota_project_id)
arithmetic1728772dac62020-03-27 14:34:13 -0700365
366 def refresh(self, request):
367 """Refreshes the access token.
368
369 Args:
370 request (google.auth.transport.Request): This argument is required
371 by the base class interface but not used in this implementation,
372 so just set it to `None`.
373
374 Raises:
375 google.auth.exceptions.UserAccessTokenError: If the access token
376 refresh failed.
377 """
378 self.token = _cloud_sdk.get_auth_access_token(self._account)
379
380 @_helpers.copy_docstring(credentials.Credentials)
381 def before_request(self, request, method, url, headers):
382 self.refresh(request)
383 self.apply(headers)