blob: 757219671894e53043155f2dfa61e3e7b19d0bf3 [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
Hiranya Jayathilaka23c88f72017-12-05 09:29:59 -080034import io
35import json
36
37import six
38
arithmetic1728772dac62020-03-27 14:34:13 -070039from google.auth import _cloud_sdk
Jon Wayne Parrott10ec7e92016-10-17 10:46:38 -070040from google.auth import _helpers
41from google.auth import credentials
Thea Flowers118c0482018-05-24 13:34:07 -070042from google.auth import exceptions
Jon Wayne Parrott10ec7e92016-10-17 10:46:38 -070043from google.oauth2 import _client
44
45
Hiranya Jayathilaka23c88f72017-12-05 09:29:59 -080046# The Google OAuth 2.0 token endpoint. Used for authorized user credentials.
Bu Sun Kim9eec0912019-10-21 17:04:21 -070047_GOOGLE_OAUTH2_TOKEN_ENDPOINT = "https://oauth2.googleapis.com/token"
Hiranya Jayathilaka23c88f72017-12-05 09:29:59 -080048
49
Mahmoud Bassiounycb7b3c42017-09-20 09:01:28 -070050class Credentials(credentials.ReadOnlyScoped, credentials.Credentials):
Bu Sun Kimb12488c2020-06-10 13:44:07 -070051 """Credentials using OAuth 2.0 access and refresh tokens.
52
53 The credentials are considered immutable. If you want to modify the
54 quota project, use :meth:`with_quota_project` or ::
55
56 credentials = credentials.with_quota_project('myproject-123)
57 """
Jon Wayne Parrott10ec7e92016-10-17 10:46:38 -070058
Bu Sun Kim9eec0912019-10-21 17:04:21 -070059 def __init__(
60 self,
61 token,
62 refresh_token=None,
63 id_token=None,
64 token_uri=None,
65 client_id=None,
66 client_secret=None,
67 scopes=None,
Bu Sun Kim32d71a52019-12-18 11:30:46 -080068 quota_project_id=None,
Bu Sun Kim9eec0912019-10-21 17:04:21 -070069 ):
Jon Wayne Parrott10ec7e92016-10-17 10:46:38 -070070 """
71 Args:
72 token (Optional(str)): The OAuth 2.0 access token. Can be None
73 if refresh information is provided.
74 refresh_token (str): The OAuth 2.0 refresh token. If specified,
75 credentials can be refreshed.
Jon Wayne Parrott26a16372017-03-28 13:03:33 -070076 id_token (str): The Open ID Connect ID Token.
Jon Wayne Parrott10ec7e92016-10-17 10:46:38 -070077 token_uri (str): The OAuth 2.0 authorization server's token
78 endpoint URI. Must be specified for refresh, can be left as
79 None if the token can not be refreshed.
80 client_id (str): The OAuth 2.0 client ID. Must be specified for
81 refresh, can be left as None if the token can not be refreshed.
82 client_secret(str): The OAuth 2.0 client secret. Must be specified
83 for refresh, can be left as None if the token can not be
84 refreshed.
Eugene W. Foley49a18c42019-05-22 13:50:38 -040085 scopes (Sequence[str]): The scopes used to obtain authorization.
86 This parameter is used by :meth:`has_scopes`. OAuth 2.0
87 credentials can not request additional scopes after
88 authorization. The scopes must be derivable from the refresh
89 token if refresh information is provided (e.g. The refresh
90 token scopes are a superset of this or contain a wild card
91 scope like 'https://www.googleapis.com/auth/any-api').
Bu Sun Kim32d71a52019-12-18 11:30:46 -080092 quota_project_id (Optional[str]): The project ID used for quota and billing.
93 This project may be different from the project used to
94 create the credentials.
Jon Wayne Parrott10ec7e92016-10-17 10:46:38 -070095 """
96 super(Credentials, self).__init__()
97 self.token = token
98 self._refresh_token = refresh_token
Jon Wayne Parrott26a16372017-03-28 13:03:33 -070099 self._id_token = id_token
Jon Wayne Parrott10ec7e92016-10-17 10:46:38 -0700100 self._scopes = scopes
101 self._token_uri = token_uri
102 self._client_id = client_id
103 self._client_secret = client_secret
Bu Sun Kim32d71a52019-12-18 11:30:46 -0800104 self._quota_project_id = quota_project_id
105
106 def __getstate__(self):
107 """A __getstate__ method must exist for the __setstate__ to be called
108 This is identical to the default implementation.
109 See https://docs.python.org/3.7/library/pickle.html#object.__setstate__
110 """
111 return self.__dict__
112
113 def __setstate__(self, d):
114 """Credentials pickled with older versions of the class do not have
115 all the attributes."""
116 self.token = d.get("token")
117 self.expiry = d.get("expiry")
118 self._refresh_token = d.get("_refresh_token")
119 self._id_token = d.get("_id_token")
120 self._scopes = d.get("_scopes")
121 self._token_uri = d.get("_token_uri")
122 self._client_id = d.get("_client_id")
123 self._client_secret = d.get("_client_secret")
124 self._quota_project_id = d.get("_quota_project_id")
Jon Wayne Parrott10ec7e92016-10-17 10:46:38 -0700125
126 @property
Jon Wayne Parrott2d0549a2017-03-01 09:27:16 -0800127 def refresh_token(self):
128 """Optional[str]: The OAuth 2.0 refresh token."""
129 return self._refresh_token
130
131 @property
132 def token_uri(self):
133 """Optional[str]: The OAuth 2.0 authorization server's token endpoint
134 URI."""
135 return self._token_uri
136
137 @property
Jon Wayne Parrott26a16372017-03-28 13:03:33 -0700138 def id_token(self):
139 """Optional[str]: The Open ID Connect ID Token.
140
141 Depending on the authorization server and the scopes requested, this
142 may be populated when credentials are obtained and updated when
143 :meth:`refresh` is called. This token is a JWT. It can be verified
144 and decoded using :func:`google.oauth2.id_token.verify_oauth2_token`.
145 """
146 return self._id_token
147
148 @property
Jon Wayne Parrott2d0549a2017-03-01 09:27:16 -0800149 def client_id(self):
150 """Optional[str]: The OAuth 2.0 client ID."""
151 return self._client_id
152
153 @property
154 def client_secret(self):
155 """Optional[str]: The OAuth 2.0 client secret."""
156 return self._client_secret
157
158 @property
Bu Sun Kim32d71a52019-12-18 11:30:46 -0800159 def quota_project_id(self):
160 """Optional[str]: The project to use for quota and billing purposes."""
161 return self._quota_project_id
162
163 @property
Jon Wayne Parrott10ec7e92016-10-17 10:46:38 -0700164 def requires_scopes(self):
165 """False: OAuth 2.0 credentials have their scopes set when
166 the initial token is requested and can not be changed."""
167 return False
168
Bu Sun Kimb12488c2020-06-10 13:44:07 -0700169 def with_quota_project(self, quota_project_id):
170 """Returns a copy of these credentials with a modified quota project
171
172 Args:
173 quota_project_id (str): The project to use for quota and
174 billing purposes
175
176 Returns:
177 google.oauth2.credentials.Credentials: A new credentials instance.
178 """
179 return self.__class__(
180 self.token,
181 refresh_token=self.refresh_token,
182 id_token=self.id_token,
183 token_uri=self.token_uri,
184 client_id=self.client_id,
185 client_secret=self.client_secret,
186 scopes=self.scopes,
187 quota_project_id=quota_project_id,
188 )
189
Jon Wayne Parrott10ec7e92016-10-17 10:46:38 -0700190 @_helpers.copy_docstring(credentials.Credentials)
191 def refresh(self, request):
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700192 if (
193 self._refresh_token is None
194 or self._token_uri is None
195 or self._client_id is None
196 or self._client_secret is None
197 ):
Thea Flowers118c0482018-05-24 13:34:07 -0700198 raise exceptions.RefreshError(
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700199 "The credentials do not contain the necessary fields need to "
200 "refresh the access token. You must specify refresh_token, "
201 "token_uri, client_id, and client_secret."
202 )
Thea Flowers118c0482018-05-24 13:34:07 -0700203
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700204 access_token, refresh_token, expiry, grant_response = _client.refresh_grant(
205 request,
206 self._token_uri,
207 self._refresh_token,
208 self._client_id,
209 self._client_secret,
210 self._scopes,
211 )
Jon Wayne Parrott10ec7e92016-10-17 10:46:38 -0700212
213 self.token = access_token
214 self.expiry = expiry
215 self._refresh_token = refresh_token
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700216 self._id_token = grant_response.get("id_token")
Hiranya Jayathilaka23c88f72017-12-05 09:29:59 -0800217
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700218 if self._scopes and "scopes" in grant_response:
Eugene W. Foley49a18c42019-05-22 13:50:38 -0400219 requested_scopes = frozenset(self._scopes)
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700220 granted_scopes = frozenset(grant_response["scopes"].split())
221 scopes_requested_but_not_granted = requested_scopes - granted_scopes
Eugene W. Foley49a18c42019-05-22 13:50:38 -0400222 if scopes_requested_but_not_granted:
223 raise exceptions.RefreshError(
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700224 "Not all requested scopes were granted by the "
225 "authorization server, missing scopes {}.".format(
226 ", ".join(scopes_requested_but_not_granted)
227 )
228 )
Eugene W. Foley49a18c42019-05-22 13:50:38 -0400229
Bu Sun Kim32d71a52019-12-18 11:30:46 -0800230 @_helpers.copy_docstring(credentials.Credentials)
231 def apply(self, headers, token=None):
232 super(Credentials, self).apply(headers, token=token)
233 if self.quota_project_id is not None:
234 headers["x-goog-user-project"] = self.quota_project_id
235
Hiranya Jayathilaka23c88f72017-12-05 09:29:59 -0800236 @classmethod
237 def from_authorized_user_info(cls, info, scopes=None):
238 """Creates a Credentials instance from parsed authorized user info.
239
240 Args:
241 info (Mapping[str, str]): The authorized user info in Google
242 format.
243 scopes (Sequence[str]): Optional list of scopes to include in the
244 credentials.
245
246 Returns:
247 google.oauth2.credentials.Credentials: The constructed
248 credentials.
249
250 Raises:
251 ValueError: If the info is not in the expected format.
252 """
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700253 keys_needed = set(("refresh_token", "client_id", "client_secret"))
Hiranya Jayathilaka23c88f72017-12-05 09:29:59 -0800254 missing = keys_needed.difference(six.iterkeys(info))
255
256 if missing:
257 raise ValueError(
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700258 "Authorized user info was not in the expected format, missing "
259 "fields {}.".format(", ".join(missing))
260 )
Hiranya Jayathilaka23c88f72017-12-05 09:29:59 -0800261
Emile Caron530f5f92019-07-26 01:23:25 +0200262 return cls(
Hiranya Jayathilaka23c88f72017-12-05 09:29:59 -0800263 None, # No access token, must be refreshed.
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700264 refresh_token=info["refresh_token"],
Hiranya Jayathilaka23c88f72017-12-05 09:29:59 -0800265 token_uri=_GOOGLE_OAUTH2_TOKEN_ENDPOINT,
266 scopes=scopes,
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700267 client_id=info["client_id"],
268 client_secret=info["client_secret"],
Bu Sun Kim32d71a52019-12-18 11:30:46 -0800269 quota_project_id=info.get(
270 "quota_project_id"
271 ), # quota project may not exist
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700272 )
Hiranya Jayathilaka23c88f72017-12-05 09:29:59 -0800273
274 @classmethod
275 def from_authorized_user_file(cls, filename, scopes=None):
276 """Creates a Credentials instance from an authorized user json file.
277
278 Args:
279 filename (str): The path to the authorized user json file.
280 scopes (Sequence[str]): Optional list of scopes to include in the
281 credentials.
282
283 Returns:
284 google.oauth2.credentials.Credentials: The constructed
285 credentials.
286
287 Raises:
288 ValueError: If the file is not in the expected format.
289 """
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700290 with io.open(filename, "r", encoding="utf-8") as json_file:
Hiranya Jayathilaka23c88f72017-12-05 09:29:59 -0800291 data = json.load(json_file)
292 return cls.from_authorized_user_info(data, scopes)
patkasperbfb1f8c2019-12-05 22:03:44 +0100293
294 def to_json(self, strip=None):
295 """Utility function that creates a JSON representation of a Credentials
296 object.
297
298 Args:
299 strip (Sequence[str]): Optional list of members to exclude from the
300 generated JSON.
301
302 Returns:
alvyjudy67d38d82020-06-10 22:23:31 -0400303 str: A JSON representation of this instance. When converted into
304 a dictionary, it can be passed to from_authorized_user_info()
305 to create a new credential instance.
patkasperbfb1f8c2019-12-05 22:03:44 +0100306 """
307 prep = {
308 "token": self.token,
309 "refresh_token": self.refresh_token,
310 "token_uri": self.token_uri,
311 "client_id": self.client_id,
312 "client_secret": self.client_secret,
313 "scopes": self.scopes,
314 }
315
316 # Remove empty entries
317 prep = {k: v for k, v in prep.items() if v is not None}
318
319 # Remove entries that explicitely need to be removed
320 if strip is not None:
321 prep = {k: v for k, v in prep.items() if k not in strip}
322
323 return json.dumps(prep)
arithmetic1728772dac62020-03-27 14:34:13 -0700324
325
326class UserAccessTokenCredentials(credentials.Credentials):
327 """Access token credentials for user account.
328
329 Obtain the access token for a given user account or the current active
330 user account with the ``gcloud auth print-access-token`` command.
331
332 Args:
333 account (Optional[str]): Account to get the access token for. If not
334 specified, the current active account will be used.
335 """
336
337 def __init__(self, account=None):
338 super(UserAccessTokenCredentials, self).__init__()
339 self._account = account
340
341 def with_account(self, account):
342 """Create a new instance with the given account.
343
344 Args:
345 account (str): Account to get the access token for.
346
347 Returns:
348 google.oauth2.credentials.UserAccessTokenCredentials: The created
349 credentials with the given account.
350 """
351 return self.__class__(account=account)
352
353 def refresh(self, request):
354 """Refreshes the access token.
355
356 Args:
357 request (google.auth.transport.Request): This argument is required
358 by the base class interface but not used in this implementation,
359 so just set it to `None`.
360
361 Raises:
362 google.auth.exceptions.UserAccessTokenError: If the access token
363 refresh failed.
364 """
365 self.token = _cloud_sdk.get_auth_access_token(self._account)
366
367 @_helpers.copy_docstring(credentials.Credentials)
368 def before_request(self, request, method, url, headers):
369 self.refresh(request)
370 self.apply(headers)