blob: 422c8ab1054acf83e01bfbf38503d630b4d15cc4 [file] [log] [blame]
Jon Wayne Parrott10ec7e92016-10-17 10:46:38 -07001# Copyright 2016 Google Inc.
2#
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 Kimab3dc1e2019-12-06 15:56:24 -050061 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 Kimab3dc1e2019-12-06 15:56:24 -050085 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 Kimab3dc1e2019-12-06 15:56:24 -050097 self._quota_project_id = quota_project_id
Jon Wayne Parrott10ec7e92016-10-17 10:46:38 -070098
99 @property
Jon Wayne Parrott2d0549a2017-03-01 09:27:16 -0800100 def refresh_token(self):
101 """Optional[str]: The OAuth 2.0 refresh token."""
102 return self._refresh_token
103
104 @property
105 def token_uri(self):
106 """Optional[str]: The OAuth 2.0 authorization server's token endpoint
107 URI."""
108 return self._token_uri
109
110 @property
Jon Wayne Parrott26a16372017-03-28 13:03:33 -0700111 def id_token(self):
112 """Optional[str]: The Open ID Connect ID Token.
113
114 Depending on the authorization server and the scopes requested, this
115 may be populated when credentials are obtained and updated when
116 :meth:`refresh` is called. This token is a JWT. It can be verified
117 and decoded using :func:`google.oauth2.id_token.verify_oauth2_token`.
118 """
119 return self._id_token
120
121 @property
Jon Wayne Parrott2d0549a2017-03-01 09:27:16 -0800122 def client_id(self):
123 """Optional[str]: The OAuth 2.0 client ID."""
124 return self._client_id
125
126 @property
127 def client_secret(self):
128 """Optional[str]: The OAuth 2.0 client secret."""
129 return self._client_secret
130
131 @property
Bu Sun Kimab3dc1e2019-12-06 15:56:24 -0500132 def quota_project_id(self):
133 """Optional[str]: The project to use for quota and billing purposes."""
134 return self._quota_project_id
135
136 @property
Jon Wayne Parrott10ec7e92016-10-17 10:46:38 -0700137 def requires_scopes(self):
138 """False: OAuth 2.0 credentials have their scopes set when
139 the initial token is requested and can not be changed."""
140 return False
141
Jon Wayne Parrott10ec7e92016-10-17 10:46:38 -0700142 @_helpers.copy_docstring(credentials.Credentials)
143 def refresh(self, request):
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700144 if (
145 self._refresh_token is None
146 or self._token_uri is None
147 or self._client_id is None
148 or self._client_secret is None
149 ):
Thea Flowers118c0482018-05-24 13:34:07 -0700150 raise exceptions.RefreshError(
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700151 "The credentials do not contain the necessary fields need to "
152 "refresh the access token. You must specify refresh_token, "
153 "token_uri, client_id, and client_secret."
154 )
Thea Flowers118c0482018-05-24 13:34:07 -0700155
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700156 access_token, refresh_token, expiry, grant_response = _client.refresh_grant(
157 request,
158 self._token_uri,
159 self._refresh_token,
160 self._client_id,
161 self._client_secret,
162 self._scopes,
163 )
Jon Wayne Parrott10ec7e92016-10-17 10:46:38 -0700164
165 self.token = access_token
166 self.expiry = expiry
167 self._refresh_token = refresh_token
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700168 self._id_token = grant_response.get("id_token")
Hiranya Jayathilaka23c88f72017-12-05 09:29:59 -0800169
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700170 if self._scopes and "scopes" in grant_response:
Eugene W. Foley49a18c42019-05-22 13:50:38 -0400171 requested_scopes = frozenset(self._scopes)
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700172 granted_scopes = frozenset(grant_response["scopes"].split())
173 scopes_requested_but_not_granted = requested_scopes - granted_scopes
Eugene W. Foley49a18c42019-05-22 13:50:38 -0400174 if scopes_requested_but_not_granted:
175 raise exceptions.RefreshError(
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700176 "Not all requested scopes were granted by the "
177 "authorization server, missing scopes {}.".format(
178 ", ".join(scopes_requested_but_not_granted)
179 )
180 )
Eugene W. Foley49a18c42019-05-22 13:50:38 -0400181
Bu Sun Kimab3dc1e2019-12-06 15:56:24 -0500182 @_helpers.copy_docstring(credentials.Credentials)
183 def apply(self, headers, token=None):
184 super(Credentials, self).apply(headers, token=token)
185 if self.quota_project_id is not None:
186 headers["x-goog-user-project"] = self.quota_project_id
187
Hiranya Jayathilaka23c88f72017-12-05 09:29:59 -0800188 @classmethod
189 def from_authorized_user_info(cls, info, scopes=None):
190 """Creates a Credentials instance from parsed authorized user info.
191
192 Args:
193 info (Mapping[str, str]): The authorized user info in Google
194 format.
195 scopes (Sequence[str]): Optional list of scopes to include in the
196 credentials.
197
198 Returns:
199 google.oauth2.credentials.Credentials: The constructed
200 credentials.
201
202 Raises:
203 ValueError: If the info is not in the expected format.
204 """
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700205 keys_needed = set(("refresh_token", "client_id", "client_secret"))
Hiranya Jayathilaka23c88f72017-12-05 09:29:59 -0800206 missing = keys_needed.difference(six.iterkeys(info))
207
208 if missing:
209 raise ValueError(
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700210 "Authorized user info was not in the expected format, missing "
211 "fields {}.".format(", ".join(missing))
212 )
Hiranya Jayathilaka23c88f72017-12-05 09:29:59 -0800213
Emile Caron530f5f92019-07-26 01:23:25 +0200214 return cls(
Hiranya Jayathilaka23c88f72017-12-05 09:29:59 -0800215 None, # No access token, must be refreshed.
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700216 refresh_token=info["refresh_token"],
Hiranya Jayathilaka23c88f72017-12-05 09:29:59 -0800217 token_uri=_GOOGLE_OAUTH2_TOKEN_ENDPOINT,
218 scopes=scopes,
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700219 client_id=info["client_id"],
220 client_secret=info["client_secret"],
Bu Sun Kimab3dc1e2019-12-06 15:56:24 -0500221 quota_project_id=info.get(
222 "quota_project_id"
223 ), # quota project may not exist
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700224 )
Hiranya Jayathilaka23c88f72017-12-05 09:29:59 -0800225
226 @classmethod
227 def from_authorized_user_file(cls, filename, scopes=None):
228 """Creates a Credentials instance from an authorized user json file.
229
230 Args:
231 filename (str): The path to the authorized user json file.
232 scopes (Sequence[str]): Optional list of scopes to include in the
233 credentials.
234
235 Returns:
236 google.oauth2.credentials.Credentials: The constructed
237 credentials.
238
239 Raises:
240 ValueError: If the file is not in the expected format.
241 """
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700242 with io.open(filename, "r", encoding="utf-8") as json_file:
Hiranya Jayathilaka23c88f72017-12-05 09:29:59 -0800243 data = json.load(json_file)
244 return cls.from_authorized_user_info(data, scopes)
patkasperbfb1f8c2019-12-05 22:03:44 +0100245
246 def to_json(self, strip=None):
247 """Utility function that creates a JSON representation of a Credentials
248 object.
249
250 Args:
251 strip (Sequence[str]): Optional list of members to exclude from the
252 generated JSON.
253
254 Returns:
255 str: A JSON representation of this instance, suitable to pass to
256 from_json().
257 """
258 prep = {
259 "token": self.token,
260 "refresh_token": self.refresh_token,
261 "token_uri": self.token_uri,
262 "client_id": self.client_id,
263 "client_secret": self.client_secret,
264 "scopes": self.scopes,
265 }
266
267 # Remove empty entries
268 prep = {k: v for k, v in prep.items() if v is not None}
269
270 # Remove entries that explicitely need to be removed
271 if strip is not None:
272 prep = {k: v for k, v in prep.items() if k not in strip}
273
274 return json.dumps(prep)