blob: 6e58f630d36b3c348407f5d4fe9ec5a9dd7dabf0 [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
Bu Sun Kim41599ae2020-09-02 12:55:42 -060050class Credentials(credentials.ReadOnlyScoped, credentials.CredentialsWithQuotaProject):
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
Jon Wayne Parrott10ec7e92016-10-17 10:46:38 -0700159 def requires_scopes(self):
160 """False: OAuth 2.0 credentials have their scopes set when
161 the initial token is requested and can not be changed."""
162 return False
163
Bu Sun Kim41599ae2020-09-02 12:55:42 -0600164 @_helpers.copy_docstring(credentials.CredentialsWithQuotaProject)
Bu Sun Kimb12488c2020-06-10 13:44:07 -0700165 def with_quota_project(self, quota_project_id):
Bu Sun Kimb12488c2020-06-10 13:44:07 -0700166
Bu Sun Kimb12488c2020-06-10 13:44:07 -0700167 return self.__class__(
168 self.token,
169 refresh_token=self.refresh_token,
170 id_token=self.id_token,
171 token_uri=self.token_uri,
172 client_id=self.client_id,
173 client_secret=self.client_secret,
174 scopes=self.scopes,
175 quota_project_id=quota_project_id,
176 )
177
Jon Wayne Parrott10ec7e92016-10-17 10:46:38 -0700178 @_helpers.copy_docstring(credentials.Credentials)
179 def refresh(self, request):
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700180 if (
181 self._refresh_token is None
182 or self._token_uri is None
183 or self._client_id is None
184 or self._client_secret is None
185 ):
Thea Flowers118c0482018-05-24 13:34:07 -0700186 raise exceptions.RefreshError(
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700187 "The credentials do not contain the necessary fields need to "
188 "refresh the access token. You must specify refresh_token, "
189 "token_uri, client_id, and client_secret."
190 )
Thea Flowers118c0482018-05-24 13:34:07 -0700191
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700192 access_token, refresh_token, expiry, grant_response = _client.refresh_grant(
193 request,
194 self._token_uri,
195 self._refresh_token,
196 self._client_id,
197 self._client_secret,
198 self._scopes,
199 )
Jon Wayne Parrott10ec7e92016-10-17 10:46:38 -0700200
201 self.token = access_token
202 self.expiry = expiry
203 self._refresh_token = refresh_token
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700204 self._id_token = grant_response.get("id_token")
Hiranya Jayathilaka23c88f72017-12-05 09:29:59 -0800205
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700206 if self._scopes and "scopes" in grant_response:
Eugene W. Foley49a18c42019-05-22 13:50:38 -0400207 requested_scopes = frozenset(self._scopes)
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700208 granted_scopes = frozenset(grant_response["scopes"].split())
209 scopes_requested_but_not_granted = requested_scopes - granted_scopes
Eugene W. Foley49a18c42019-05-22 13:50:38 -0400210 if scopes_requested_but_not_granted:
211 raise exceptions.RefreshError(
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700212 "Not all requested scopes were granted by the "
213 "authorization server, missing scopes {}.".format(
214 ", ".join(scopes_requested_but_not_granted)
215 )
216 )
Eugene W. Foley49a18c42019-05-22 13:50:38 -0400217
Hiranya Jayathilaka23c88f72017-12-05 09:29:59 -0800218 @classmethod
219 def from_authorized_user_info(cls, info, scopes=None):
220 """Creates a Credentials instance from parsed authorized user info.
221
222 Args:
223 info (Mapping[str, str]): The authorized user info in Google
224 format.
225 scopes (Sequence[str]): Optional list of scopes to include in the
226 credentials.
227
228 Returns:
229 google.oauth2.credentials.Credentials: The constructed
230 credentials.
231
232 Raises:
233 ValueError: If the info is not in the expected format.
234 """
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700235 keys_needed = set(("refresh_token", "client_id", "client_secret"))
Hiranya Jayathilaka23c88f72017-12-05 09:29:59 -0800236 missing = keys_needed.difference(six.iterkeys(info))
237
238 if missing:
239 raise ValueError(
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700240 "Authorized user info was not in the expected format, missing "
241 "fields {}.".format(", ".join(missing))
242 )
Hiranya Jayathilaka23c88f72017-12-05 09:29:59 -0800243
Emile Caron530f5f92019-07-26 01:23:25 +0200244 return cls(
Hiranya Jayathilaka23c88f72017-12-05 09:29:59 -0800245 None, # No access token, must be refreshed.
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700246 refresh_token=info["refresh_token"],
Hiranya Jayathilaka23c88f72017-12-05 09:29:59 -0800247 token_uri=_GOOGLE_OAUTH2_TOKEN_ENDPOINT,
248 scopes=scopes,
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700249 client_id=info["client_id"],
250 client_secret=info["client_secret"],
Bu Sun Kim32d71a52019-12-18 11:30:46 -0800251 quota_project_id=info.get(
252 "quota_project_id"
253 ), # quota project may not exist
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700254 )
Hiranya Jayathilaka23c88f72017-12-05 09:29:59 -0800255
256 @classmethod
257 def from_authorized_user_file(cls, filename, scopes=None):
258 """Creates a Credentials instance from an authorized user json file.
259
260 Args:
261 filename (str): The path to the authorized user json file.
262 scopes (Sequence[str]): Optional list of scopes to include in the
263 credentials.
264
265 Returns:
266 google.oauth2.credentials.Credentials: The constructed
267 credentials.
268
269 Raises:
270 ValueError: If the file is not in the expected format.
271 """
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700272 with io.open(filename, "r", encoding="utf-8") as json_file:
Hiranya Jayathilaka23c88f72017-12-05 09:29:59 -0800273 data = json.load(json_file)
274 return cls.from_authorized_user_info(data, scopes)
patkasperbfb1f8c2019-12-05 22:03:44 +0100275
276 def to_json(self, strip=None):
277 """Utility function that creates a JSON representation of a Credentials
278 object.
279
280 Args:
281 strip (Sequence[str]): Optional list of members to exclude from the
282 generated JSON.
283
284 Returns:
alvyjudy67d38d82020-06-10 22:23:31 -0400285 str: A JSON representation of this instance. When converted into
286 a dictionary, it can be passed to from_authorized_user_info()
287 to create a new credential instance.
patkasperbfb1f8c2019-12-05 22:03:44 +0100288 """
289 prep = {
290 "token": self.token,
291 "refresh_token": self.refresh_token,
292 "token_uri": self.token_uri,
293 "client_id": self.client_id,
294 "client_secret": self.client_secret,
295 "scopes": self.scopes,
296 }
297
298 # Remove empty entries
299 prep = {k: v for k, v in prep.items() if v is not None}
300
301 # Remove entries that explicitely need to be removed
302 if strip is not None:
303 prep = {k: v for k, v in prep.items() if k not in strip}
304
305 return json.dumps(prep)
arithmetic1728772dac62020-03-27 14:34:13 -0700306
307
Bu Sun Kim41599ae2020-09-02 12:55:42 -0600308class UserAccessTokenCredentials(credentials.CredentialsWithQuotaProject):
arithmetic1728772dac62020-03-27 14:34:13 -0700309 """Access token credentials for user account.
310
311 Obtain the access token for a given user account or the current active
312 user account with the ``gcloud auth print-access-token`` command.
313
314 Args:
315 account (Optional[str]): Account to get the access token for. If not
316 specified, the current active account will be used.
Bu Sun Kim3dda7b22020-07-09 10:39:39 -0700317 quota_project_id (Optional[str]): The project ID used for quota
318 and billing.
319
arithmetic1728772dac62020-03-27 14:34:13 -0700320 """
321
Bu Sun Kim3dda7b22020-07-09 10:39:39 -0700322 def __init__(self, account=None, quota_project_id=None):
arithmetic1728772dac62020-03-27 14:34:13 -0700323 super(UserAccessTokenCredentials, self).__init__()
324 self._account = account
Bu Sun Kim3dda7b22020-07-09 10:39:39 -0700325 self._quota_project_id = quota_project_id
arithmetic1728772dac62020-03-27 14:34:13 -0700326
327 def with_account(self, account):
328 """Create a new instance with the given account.
329
330 Args:
331 account (str): Account to get the access token for.
332
333 Returns:
334 google.oauth2.credentials.UserAccessTokenCredentials: The created
335 credentials with the given account.
336 """
Bu Sun Kim3dda7b22020-07-09 10:39:39 -0700337 return self.__class__(account=account, quota_project_id=self._quota_project_id)
338
Bu Sun Kim41599ae2020-09-02 12:55:42 -0600339 @_helpers.copy_docstring(credentials.CredentialsWithQuotaProject)
Bu Sun Kim3dda7b22020-07-09 10:39:39 -0700340 def with_quota_project(self, quota_project_id):
341 return self.__class__(account=self._account, quota_project_id=quota_project_id)
arithmetic1728772dac62020-03-27 14:34:13 -0700342
343 def refresh(self, request):
344 """Refreshes the access token.
345
346 Args:
347 request (google.auth.transport.Request): This argument is required
348 by the base class interface but not used in this implementation,
349 so just set it to `None`.
350
351 Raises:
352 google.auth.exceptions.UserAccessTokenError: If the access token
353 refresh failed.
354 """
355 self.token = _cloud_sdk.get_auth_access_token(self._account)
356
357 @_helpers.copy_docstring(credentials.Credentials)
358 def before_request(self, request, method, url, headers):
359 self.refresh(request)
360 self.apply(headers)