blob: baf3cf7f48ccc342408a8fcc5753bac211fd9e91 [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):
Jon Wayne Parrott10ec7e92016-10-17 10:46:38 -070051 """Credentials using OAuth 2.0 access and refresh tokens."""
52
Bu Sun Kim9eec0912019-10-21 17:04:21 -070053 def __init__(
54 self,
55 token,
56 refresh_token=None,
57 id_token=None,
58 token_uri=None,
59 client_id=None,
60 client_secret=None,
61 scopes=None,
Bu Sun Kim32d71a52019-12-18 11:30:46 -080062 quota_project_id=None,
Bu Sun Kim9eec0912019-10-21 17:04:21 -070063 ):
Jon Wayne Parrott10ec7e92016-10-17 10:46:38 -070064 """
65 Args:
66 token (Optional(str)): The OAuth 2.0 access token. Can be None
67 if refresh information is provided.
68 refresh_token (str): The OAuth 2.0 refresh token. If specified,
69 credentials can be refreshed.
Jon Wayne Parrott26a16372017-03-28 13:03:33 -070070 id_token (str): The Open ID Connect ID Token.
Jon Wayne Parrott10ec7e92016-10-17 10:46:38 -070071 token_uri (str): The OAuth 2.0 authorization server's token
72 endpoint URI. Must be specified for refresh, can be left as
73 None if the token can not be refreshed.
74 client_id (str): The OAuth 2.0 client ID. Must be specified for
75 refresh, can be left as None if the token can not be refreshed.
76 client_secret(str): The OAuth 2.0 client secret. Must be specified
77 for refresh, can be left as None if the token can not be
78 refreshed.
Eugene W. Foley49a18c42019-05-22 13:50:38 -040079 scopes (Sequence[str]): The scopes used to obtain authorization.
80 This parameter is used by :meth:`has_scopes`. OAuth 2.0
81 credentials can not request additional scopes after
82 authorization. The scopes must be derivable from the refresh
83 token if refresh information is provided (e.g. The refresh
84 token scopes are a superset of this or contain a wild card
85 scope like 'https://www.googleapis.com/auth/any-api').
Bu Sun Kim32d71a52019-12-18 11:30:46 -080086 quota_project_id (Optional[str]): The project ID used for quota and billing.
87 This project may be different from the project used to
88 create the credentials.
Jon Wayne Parrott10ec7e92016-10-17 10:46:38 -070089 """
90 super(Credentials, self).__init__()
91 self.token = token
92 self._refresh_token = refresh_token
Jon Wayne Parrott26a16372017-03-28 13:03:33 -070093 self._id_token = id_token
Jon Wayne Parrott10ec7e92016-10-17 10:46:38 -070094 self._scopes = scopes
95 self._token_uri = token_uri
96 self._client_id = client_id
97 self._client_secret = client_secret
Bu Sun Kim32d71a52019-12-18 11:30:46 -080098 self._quota_project_id = quota_project_id
99
100 def __getstate__(self):
101 """A __getstate__ method must exist for the __setstate__ to be called
102 This is identical to the default implementation.
103 See https://docs.python.org/3.7/library/pickle.html#object.__setstate__
104 """
105 return self.__dict__
106
107 def __setstate__(self, d):
108 """Credentials pickled with older versions of the class do not have
109 all the attributes."""
110 self.token = d.get("token")
111 self.expiry = d.get("expiry")
112 self._refresh_token = d.get("_refresh_token")
113 self._id_token = d.get("_id_token")
114 self._scopes = d.get("_scopes")
115 self._token_uri = d.get("_token_uri")
116 self._client_id = d.get("_client_id")
117 self._client_secret = d.get("_client_secret")
118 self._quota_project_id = d.get("_quota_project_id")
Jon Wayne Parrott10ec7e92016-10-17 10:46:38 -0700119
120 @property
Jon Wayne Parrott2d0549a2017-03-01 09:27:16 -0800121 def refresh_token(self):
122 """Optional[str]: The OAuth 2.0 refresh token."""
123 return self._refresh_token
124
125 @property
126 def token_uri(self):
127 """Optional[str]: The OAuth 2.0 authorization server's token endpoint
128 URI."""
129 return self._token_uri
130
131 @property
Jon Wayne Parrott26a16372017-03-28 13:03:33 -0700132 def id_token(self):
133 """Optional[str]: The Open ID Connect ID Token.
134
135 Depending on the authorization server and the scopes requested, this
136 may be populated when credentials are obtained and updated when
137 :meth:`refresh` is called. This token is a JWT. It can be verified
138 and decoded using :func:`google.oauth2.id_token.verify_oauth2_token`.
139 """
140 return self._id_token
141
142 @property
Jon Wayne Parrott2d0549a2017-03-01 09:27:16 -0800143 def client_id(self):
144 """Optional[str]: The OAuth 2.0 client ID."""
145 return self._client_id
146
147 @property
148 def client_secret(self):
149 """Optional[str]: The OAuth 2.0 client secret."""
150 return self._client_secret
151
152 @property
Bu Sun Kim32d71a52019-12-18 11:30:46 -0800153 def quota_project_id(self):
154 """Optional[str]: The project to use for quota and billing purposes."""
155 return self._quota_project_id
156
157 @property
Jon Wayne Parrott10ec7e92016-10-17 10:46:38 -0700158 def requires_scopes(self):
159 """False: OAuth 2.0 credentials have their scopes set when
160 the initial token is requested and can not be changed."""
161 return False
162
Jon Wayne Parrott10ec7e92016-10-17 10:46:38 -0700163 @_helpers.copy_docstring(credentials.Credentials)
164 def refresh(self, request):
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700165 if (
166 self._refresh_token is None
167 or self._token_uri is None
168 or self._client_id is None
169 or self._client_secret is None
170 ):
Thea Flowers118c0482018-05-24 13:34:07 -0700171 raise exceptions.RefreshError(
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700172 "The credentials do not contain the necessary fields need to "
173 "refresh the access token. You must specify refresh_token, "
174 "token_uri, client_id, and client_secret."
175 )
Thea Flowers118c0482018-05-24 13:34:07 -0700176
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700177 access_token, refresh_token, expiry, grant_response = _client.refresh_grant(
178 request,
179 self._token_uri,
180 self._refresh_token,
181 self._client_id,
182 self._client_secret,
183 self._scopes,
184 )
Jon Wayne Parrott10ec7e92016-10-17 10:46:38 -0700185
186 self.token = access_token
187 self.expiry = expiry
188 self._refresh_token = refresh_token
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700189 self._id_token = grant_response.get("id_token")
Hiranya Jayathilaka23c88f72017-12-05 09:29:59 -0800190
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700191 if self._scopes and "scopes" in grant_response:
Eugene W. Foley49a18c42019-05-22 13:50:38 -0400192 requested_scopes = frozenset(self._scopes)
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700193 granted_scopes = frozenset(grant_response["scopes"].split())
194 scopes_requested_but_not_granted = requested_scopes - granted_scopes
Eugene W. Foley49a18c42019-05-22 13:50:38 -0400195 if scopes_requested_but_not_granted:
196 raise exceptions.RefreshError(
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700197 "Not all requested scopes were granted by the "
198 "authorization server, missing scopes {}.".format(
199 ", ".join(scopes_requested_but_not_granted)
200 )
201 )
Eugene W. Foley49a18c42019-05-22 13:50:38 -0400202
Bu Sun Kim32d71a52019-12-18 11:30:46 -0800203 @_helpers.copy_docstring(credentials.Credentials)
204 def apply(self, headers, token=None):
205 super(Credentials, self).apply(headers, token=token)
206 if self.quota_project_id is not None:
207 headers["x-goog-user-project"] = self.quota_project_id
208
Hiranya Jayathilaka23c88f72017-12-05 09:29:59 -0800209 @classmethod
210 def from_authorized_user_info(cls, info, scopes=None):
211 """Creates a Credentials instance from parsed authorized user info.
212
213 Args:
214 info (Mapping[str, str]): The authorized user info in Google
215 format.
216 scopes (Sequence[str]): Optional list of scopes to include in the
217 credentials.
218
219 Returns:
220 google.oauth2.credentials.Credentials: The constructed
221 credentials.
222
223 Raises:
224 ValueError: If the info is not in the expected format.
225 """
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700226 keys_needed = set(("refresh_token", "client_id", "client_secret"))
Hiranya Jayathilaka23c88f72017-12-05 09:29:59 -0800227 missing = keys_needed.difference(six.iterkeys(info))
228
229 if missing:
230 raise ValueError(
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700231 "Authorized user info was not in the expected format, missing "
232 "fields {}.".format(", ".join(missing))
233 )
Hiranya Jayathilaka23c88f72017-12-05 09:29:59 -0800234
Emile Caron530f5f92019-07-26 01:23:25 +0200235 return cls(
Hiranya Jayathilaka23c88f72017-12-05 09:29:59 -0800236 None, # No access token, must be refreshed.
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700237 refresh_token=info["refresh_token"],
Hiranya Jayathilaka23c88f72017-12-05 09:29:59 -0800238 token_uri=_GOOGLE_OAUTH2_TOKEN_ENDPOINT,
239 scopes=scopes,
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700240 client_id=info["client_id"],
241 client_secret=info["client_secret"],
Bu Sun Kim32d71a52019-12-18 11:30:46 -0800242 quota_project_id=info.get(
243 "quota_project_id"
244 ), # quota project may not exist
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700245 )
Hiranya Jayathilaka23c88f72017-12-05 09:29:59 -0800246
247 @classmethod
248 def from_authorized_user_file(cls, filename, scopes=None):
249 """Creates a Credentials instance from an authorized user json file.
250
251 Args:
252 filename (str): The path to the authorized user json file.
253 scopes (Sequence[str]): Optional list of scopes to include in the
254 credentials.
255
256 Returns:
257 google.oauth2.credentials.Credentials: The constructed
258 credentials.
259
260 Raises:
261 ValueError: If the file is not in the expected format.
262 """
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700263 with io.open(filename, "r", encoding="utf-8") as json_file:
Hiranya Jayathilaka23c88f72017-12-05 09:29:59 -0800264 data = json.load(json_file)
265 return cls.from_authorized_user_info(data, scopes)
patkasperbfb1f8c2019-12-05 22:03:44 +0100266
267 def to_json(self, strip=None):
268 """Utility function that creates a JSON representation of a Credentials
269 object.
270
271 Args:
272 strip (Sequence[str]): Optional list of members to exclude from the
273 generated JSON.
274
275 Returns:
276 str: A JSON representation of this instance, suitable to pass to
277 from_json().
278 """
279 prep = {
280 "token": self.token,
281 "refresh_token": self.refresh_token,
282 "token_uri": self.token_uri,
283 "client_id": self.client_id,
284 "client_secret": self.client_secret,
285 "scopes": self.scopes,
286 }
287
288 # Remove empty entries
289 prep = {k: v for k, v in prep.items() if v is not None}
290
291 # Remove entries that explicitely need to be removed
292 if strip is not None:
293 prep = {k: v for k, v in prep.items() if k not in strip}
294
295 return json.dumps(prep)
arithmetic1728772dac62020-03-27 14:34:13 -0700296
297
298class UserAccessTokenCredentials(credentials.Credentials):
299 """Access token credentials for user account.
300
301 Obtain the access token for a given user account or the current active
302 user account with the ``gcloud auth print-access-token`` command.
303
304 Args:
305 account (Optional[str]): Account to get the access token for. If not
306 specified, the current active account will be used.
307 """
308
309 def __init__(self, account=None):
310 super(UserAccessTokenCredentials, self).__init__()
311 self._account = account
312
313 def with_account(self, account):
314 """Create a new instance with the given account.
315
316 Args:
317 account (str): Account to get the access token for.
318
319 Returns:
320 google.oauth2.credentials.UserAccessTokenCredentials: The created
321 credentials with the given account.
322 """
323 return self.__class__(account=account)
324
325 def refresh(self, request):
326 """Refreshes the access token.
327
328 Args:
329 request (google.auth.transport.Request): This argument is required
330 by the base class interface but not used in this implementation,
331 so just set it to `None`.
332
333 Raises:
334 google.auth.exceptions.UserAccessTokenError: If the access token
335 refresh failed.
336 """
337 self.token = _cloud_sdk.get_auth_access_token(self._account)
338
339 @_helpers.copy_docstring(credentials.Credentials)
340 def before_request(self, request, method, url, headers):
341 self.refresh(request)
342 self.apply(headers)