blob: 1adcbf6755f9c4881befa358552270925cba0b3a [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
Jon Wayne Parrott10ec7e92016-10-17 10:46:38 -070039from google.auth import _helpers
40from google.auth import credentials
Thea Flowers118c0482018-05-24 13:34:07 -070041from google.auth import exceptions
Jon Wayne Parrott10ec7e92016-10-17 10:46:38 -070042from google.oauth2 import _client
43
44
Hiranya Jayathilaka23c88f72017-12-05 09:29:59 -080045# The Google OAuth 2.0 token endpoint. Used for authorized user credentials.
Bu Sun Kim9eec0912019-10-21 17:04:21 -070046_GOOGLE_OAUTH2_TOKEN_ENDPOINT = "https://oauth2.googleapis.com/token"
Hiranya Jayathilaka23c88f72017-12-05 09:29:59 -080047
48
Mahmoud Bassiounycb7b3c42017-09-20 09:01:28 -070049class Credentials(credentials.ReadOnlyScoped, credentials.Credentials):
Jon Wayne Parrott10ec7e92016-10-17 10:46:38 -070050 """Credentials using OAuth 2.0 access and refresh tokens."""
51
Bu Sun Kim9eec0912019-10-21 17:04:21 -070052 def __init__(
53 self,
54 token,
55 refresh_token=None,
56 id_token=None,
57 token_uri=None,
58 client_id=None,
59 client_secret=None,
60 scopes=None,
Bu Sun Kim32d71a52019-12-18 11:30:46 -080061 quota_project_id=None,
Bu Sun Kim9eec0912019-10-21 17:04:21 -070062 ):
Jon Wayne Parrott10ec7e92016-10-17 10:46:38 -070063 """
64 Args:
65 token (Optional(str)): The OAuth 2.0 access token. Can be None
66 if refresh information is provided.
67 refresh_token (str): The OAuth 2.0 refresh token. If specified,
68 credentials can be refreshed.
Jon Wayne Parrott26a16372017-03-28 13:03:33 -070069 id_token (str): The Open ID Connect ID Token.
Jon Wayne Parrott10ec7e92016-10-17 10:46:38 -070070 token_uri (str): The OAuth 2.0 authorization server's token
71 endpoint URI. Must be specified for refresh, can be left as
72 None if the token can not be refreshed.
73 client_id (str): The OAuth 2.0 client ID. Must be specified for
74 refresh, can be left as None if the token can not be refreshed.
75 client_secret(str): The OAuth 2.0 client secret. Must be specified
76 for refresh, can be left as None if the token can not be
77 refreshed.
Eugene W. Foley49a18c42019-05-22 13:50:38 -040078 scopes (Sequence[str]): The scopes used to obtain authorization.
79 This parameter is used by :meth:`has_scopes`. OAuth 2.0
80 credentials can not request additional scopes after
81 authorization. The scopes must be derivable from the refresh
82 token if refresh information is provided (e.g. The refresh
83 token scopes are a superset of this or contain a wild card
84 scope like 'https://www.googleapis.com/auth/any-api').
Bu Sun Kim32d71a52019-12-18 11:30:46 -080085 quota_project_id (Optional[str]): The project ID used for quota and billing.
86 This project may be different from the project used to
87 create the credentials.
Jon Wayne Parrott10ec7e92016-10-17 10:46:38 -070088 """
89 super(Credentials, self).__init__()
90 self.token = token
91 self._refresh_token = refresh_token
Jon Wayne Parrott26a16372017-03-28 13:03:33 -070092 self._id_token = id_token
Jon Wayne Parrott10ec7e92016-10-17 10:46:38 -070093 self._scopes = scopes
94 self._token_uri = token_uri
95 self._client_id = client_id
96 self._client_secret = client_secret
Bu Sun Kim32d71a52019-12-18 11:30:46 -080097 self._quota_project_id = quota_project_id
98
99 def __getstate__(self):
100 """A __getstate__ method must exist for the __setstate__ to be called
101 This is identical to the default implementation.
102 See https://docs.python.org/3.7/library/pickle.html#object.__setstate__
103 """
104 return self.__dict__
105
106 def __setstate__(self, d):
107 """Credentials pickled with older versions of the class do not have
108 all the attributes."""
109 self.token = d.get("token")
110 self.expiry = d.get("expiry")
111 self._refresh_token = d.get("_refresh_token")
112 self._id_token = d.get("_id_token")
113 self._scopes = d.get("_scopes")
114 self._token_uri = d.get("_token_uri")
115 self._client_id = d.get("_client_id")
116 self._client_secret = d.get("_client_secret")
117 self._quota_project_id = d.get("_quota_project_id")
Jon Wayne Parrott10ec7e92016-10-17 10:46:38 -0700118
119 @property
Jon Wayne Parrott2d0549a2017-03-01 09:27:16 -0800120 def refresh_token(self):
121 """Optional[str]: The OAuth 2.0 refresh token."""
122 return self._refresh_token
123
124 @property
125 def token_uri(self):
126 """Optional[str]: The OAuth 2.0 authorization server's token endpoint
127 URI."""
128 return self._token_uri
129
130 @property
Jon Wayne Parrott26a16372017-03-28 13:03:33 -0700131 def id_token(self):
132 """Optional[str]: The Open ID Connect ID Token.
133
134 Depending on the authorization server and the scopes requested, this
135 may be populated when credentials are obtained and updated when
136 :meth:`refresh` is called. This token is a JWT. It can be verified
137 and decoded using :func:`google.oauth2.id_token.verify_oauth2_token`.
138 """
139 return self._id_token
140
141 @property
Jon Wayne Parrott2d0549a2017-03-01 09:27:16 -0800142 def client_id(self):
143 """Optional[str]: The OAuth 2.0 client ID."""
144 return self._client_id
145
146 @property
147 def client_secret(self):
148 """Optional[str]: The OAuth 2.0 client secret."""
149 return self._client_secret
150
151 @property
Bu Sun Kim32d71a52019-12-18 11:30:46 -0800152 def quota_project_id(self):
153 """Optional[str]: The project to use for quota and billing purposes."""
154 return self._quota_project_id
155
156 @property
Jon Wayne Parrott10ec7e92016-10-17 10:46:38 -0700157 def requires_scopes(self):
158 """False: OAuth 2.0 credentials have their scopes set when
159 the initial token is requested and can not be changed."""
160 return False
161
Jon Wayne Parrott10ec7e92016-10-17 10:46:38 -0700162 @_helpers.copy_docstring(credentials.Credentials)
163 def refresh(self, request):
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700164 if (
165 self._refresh_token is None
166 or self._token_uri is None
167 or self._client_id is None
168 or self._client_secret is None
169 ):
Thea Flowers118c0482018-05-24 13:34:07 -0700170 raise exceptions.RefreshError(
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700171 "The credentials do not contain the necessary fields need to "
172 "refresh the access token. You must specify refresh_token, "
173 "token_uri, client_id, and client_secret."
174 )
Thea Flowers118c0482018-05-24 13:34:07 -0700175
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700176 access_token, refresh_token, expiry, grant_response = _client.refresh_grant(
177 request,
178 self._token_uri,
179 self._refresh_token,
180 self._client_id,
181 self._client_secret,
182 self._scopes,
183 )
Jon Wayne Parrott10ec7e92016-10-17 10:46:38 -0700184
185 self.token = access_token
186 self.expiry = expiry
187 self._refresh_token = refresh_token
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700188 self._id_token = grant_response.get("id_token")
Hiranya Jayathilaka23c88f72017-12-05 09:29:59 -0800189
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700190 if self._scopes and "scopes" in grant_response:
Eugene W. Foley49a18c42019-05-22 13:50:38 -0400191 requested_scopes = frozenset(self._scopes)
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700192 granted_scopes = frozenset(grant_response["scopes"].split())
193 scopes_requested_but_not_granted = requested_scopes - granted_scopes
Eugene W. Foley49a18c42019-05-22 13:50:38 -0400194 if scopes_requested_but_not_granted:
195 raise exceptions.RefreshError(
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700196 "Not all requested scopes were granted by the "
197 "authorization server, missing scopes {}.".format(
198 ", ".join(scopes_requested_but_not_granted)
199 )
200 )
Eugene W. Foley49a18c42019-05-22 13:50:38 -0400201
Bu Sun Kim32d71a52019-12-18 11:30:46 -0800202 @_helpers.copy_docstring(credentials.Credentials)
203 def apply(self, headers, token=None):
204 super(Credentials, self).apply(headers, token=token)
205 if self.quota_project_id is not None:
206 headers["x-goog-user-project"] = self.quota_project_id
207
Hiranya Jayathilaka23c88f72017-12-05 09:29:59 -0800208 @classmethod
209 def from_authorized_user_info(cls, info, scopes=None):
210 """Creates a Credentials instance from parsed authorized user info.
211
212 Args:
213 info (Mapping[str, str]): The authorized user info in Google
214 format.
215 scopes (Sequence[str]): Optional list of scopes to include in the
216 credentials.
217
218 Returns:
219 google.oauth2.credentials.Credentials: The constructed
220 credentials.
221
222 Raises:
223 ValueError: If the info is not in the expected format.
224 """
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700225 keys_needed = set(("refresh_token", "client_id", "client_secret"))
Hiranya Jayathilaka23c88f72017-12-05 09:29:59 -0800226 missing = keys_needed.difference(six.iterkeys(info))
227
228 if missing:
229 raise ValueError(
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700230 "Authorized user info was not in the expected format, missing "
231 "fields {}.".format(", ".join(missing))
232 )
Hiranya Jayathilaka23c88f72017-12-05 09:29:59 -0800233
Emile Caron530f5f92019-07-26 01:23:25 +0200234 return cls(
Hiranya Jayathilaka23c88f72017-12-05 09:29:59 -0800235 None, # No access token, must be refreshed.
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700236 refresh_token=info["refresh_token"],
Hiranya Jayathilaka23c88f72017-12-05 09:29:59 -0800237 token_uri=_GOOGLE_OAUTH2_TOKEN_ENDPOINT,
238 scopes=scopes,
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700239 client_id=info["client_id"],
240 client_secret=info["client_secret"],
Bu Sun Kim32d71a52019-12-18 11:30:46 -0800241 quota_project_id=info.get(
242 "quota_project_id"
243 ), # quota project may not exist
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700244 )
Hiranya Jayathilaka23c88f72017-12-05 09:29:59 -0800245
246 @classmethod
247 def from_authorized_user_file(cls, filename, scopes=None):
248 """Creates a Credentials instance from an authorized user json file.
249
250 Args:
251 filename (str): The path to the authorized user json file.
252 scopes (Sequence[str]): Optional list of scopes to include in the
253 credentials.
254
255 Returns:
256 google.oauth2.credentials.Credentials: The constructed
257 credentials.
258
259 Raises:
260 ValueError: If the file is not in the expected format.
261 """
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700262 with io.open(filename, "r", encoding="utf-8") as json_file:
Hiranya Jayathilaka23c88f72017-12-05 09:29:59 -0800263 data = json.load(json_file)
264 return cls.from_authorized_user_info(data, scopes)
patkasperbfb1f8c2019-12-05 22:03:44 +0100265
266 def to_json(self, strip=None):
267 """Utility function that creates a JSON representation of a Credentials
268 object.
269
270 Args:
271 strip (Sequence[str]): Optional list of members to exclude from the
272 generated JSON.
273
274 Returns:
275 str: A JSON representation of this instance, suitable to pass to
276 from_json().
277 """
278 prep = {
279 "token": self.token,
280 "refresh_token": self.refresh_token,
281 "token_uri": self.token_uri,
282 "client_id": self.client_id,
283 "client_secret": self.client_secret,
284 "scopes": self.scopes,
285 }
286
287 # Remove empty entries
288 prep = {k: v for k, v in prep.items() if v is not None}
289
290 # Remove entries that explicitely need to be removed
291 if strip is not None:
292 prep = {k: v for k, v in prep.items() if k not in strip}
293
294 return json.dumps(prep)