blob: cd03e38d969f9b65e7169b7b8cd4bee597ac4562 [file] [log] [blame]
Joe Gregorio20a5aa92011-04-01 17:44:25 -04001# Copyright (C) 2010 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.
Joe Gregorio695fdc12011-01-16 16:46:55 -050014
Joe Gregorio9da2ad82011-09-11 14:04:44 -040015"""An OAuth 2.0 client.
Joe Gregorio695fdc12011-01-16 16:46:55 -050016
Joe Gregorio9da2ad82011-09-11 14:04:44 -040017Tools for interacting with OAuth 2.0 protected resources.
Joe Gregorio695fdc12011-01-16 16:46:55 -050018"""
19
20__author__ = 'jcgregorio@google.com (Joe Gregorio)'
21
Joe Gregorio8b4c1732011-12-06 11:28:29 -050022import base64
Joe Gregoriof08a4982011-10-07 13:11:16 -040023import clientsecrets
Joe Gregorio695fdc12011-01-16 16:46:55 -050024import copy
25import datetime
26import httplib2
27import logging
Joe Gregorio8b4c1732011-12-06 11:28:29 -050028import os
Joe Gregoriof08a4982011-10-07 13:11:16 -040029import sys
Joe Gregorio8b4c1732011-12-06 11:28:29 -050030import time
Joe Gregorio695fdc12011-01-16 16:46:55 -050031import urllib
32import urlparse
33
Joe Gregorio549230c2012-01-11 10:38:05 -050034from anyjson import simplejson
Joe Gregorio8b4c1732011-12-06 11:28:29 -050035
36HAS_OPENSSL = False
37try:
38 from oauth2client.crypt import Signer
39 from oauth2client.crypt import make_signed_jwt
40 from oauth2client.crypt import verify_signed_jwt_with_certs
41 HAS_OPENSSL = True
42except ImportError:
43 pass
44
Joe Gregorio695fdc12011-01-16 16:46:55 -050045try:
Joe Gregorio9da2ad82011-09-11 14:04:44 -040046 from urlparse import parse_qsl
Joe Gregorio695fdc12011-01-16 16:46:55 -050047except ImportError:
Joe Gregorio9da2ad82011-09-11 14:04:44 -040048 from cgi import parse_qsl
49
50logger = logging.getLogger(__name__)
Joe Gregorio695fdc12011-01-16 16:46:55 -050051
Joe Gregorio562b7312011-09-15 09:06:38 -040052# Expiry is stored in RFC3339 UTC format
Joe Gregorio8b4c1732011-12-06 11:28:29 -050053EXPIRY_FORMAT = '%Y-%m-%dT%H:%M:%SZ'
54
55# Which certs to use to validate id_tokens received.
56ID_TOKEN_VERIFICATON_CERTS = 'https://www.googleapis.com/oauth2/v1/certs'
Joe Gregorio562b7312011-09-15 09:06:38 -040057
Joe Gregorio695fdc12011-01-16 16:46:55 -050058
59class Error(Exception):
60 """Base error for this module."""
61 pass
62
63
Joe Gregorioccc79542011-02-19 00:05:26 -050064class FlowExchangeError(Error):
Joe Gregorioca876e42011-02-22 19:39:42 -050065 """Error trying to exchange an authorization grant for an access token."""
Joe Gregorioccc79542011-02-19 00:05:26 -050066 pass
67
68
69class AccessTokenRefreshError(Error):
Joe Gregorioca876e42011-02-22 19:39:42 -050070 """Error trying to refresh an expired access token."""
Joe Gregorio695fdc12011-01-16 16:46:55 -050071 pass
72
Joe Gregoriof08a4982011-10-07 13:11:16 -040073class UnknownClientSecretsFlowError(Error):
74 """The client secrets file called for an unknown type of OAuth 2.0 flow. """
75 pass
76
Joe Gregorio695fdc12011-01-16 16:46:55 -050077
Joe Gregorio3b79fa82011-02-17 11:47:17 -050078class AccessTokenCredentialsError(Error):
79 """Having only the access_token means no refresh is possible."""
Joe Gregorio695fdc12011-01-16 16:46:55 -050080 pass
81
82
Joe Gregorio8b4c1732011-12-06 11:28:29 -050083class VerifyJwtTokenError(Error):
84 """Could on retrieve certificates for validation."""
85 pass
86
87
Joe Gregorio695fdc12011-01-16 16:46:55 -050088def _abstract():
89 raise NotImplementedError('You need to override this function')
90
91
92class Credentials(object):
93 """Base class for all Credentials objects.
94
Joe Gregorio562b7312011-09-15 09:06:38 -040095 Subclasses must define an authorize() method that applies the credentials to
96 an HTTP transport.
97
98 Subclasses must also specify a classmethod named 'from_json' that takes a JSON
99 string as input and returns an instaniated Crentials object.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500100 """
101
Joe Gregorio562b7312011-09-15 09:06:38 -0400102 NON_SERIALIZED_MEMBERS = ['store']
103
Joe Gregorio695fdc12011-01-16 16:46:55 -0500104 def authorize(self, http):
105 """Take an httplib2.Http instance (or equivalent) and
106 authorizes it for the set of credentials, usually by
107 replacing http.request() with a method that adds in
108 the appropriate headers and then delegates to the original
109 Http.request() method.
110 """
111 _abstract()
112
Joe Gregorio562b7312011-09-15 09:06:38 -0400113 def _to_json(self, strip):
114 """Utility function for creating a JSON representation of an instance of Credentials.
115
116 Args:
117 strip: array, An array of names of members to not include in the JSON.
118
119 Returns:
120 string, a JSON representation of this instance, suitable to pass to
121 from_json().
122 """
123 t = type(self)
124 d = copy.copy(self.__dict__)
125 for member in strip:
126 del d[member]
127 if 'token_expiry' in d and isinstance(d['token_expiry'], datetime.datetime):
128 d['token_expiry'] = d['token_expiry'].strftime(EXPIRY_FORMAT)
129 # Add in information we will need later to reconsistitue this instance.
130 d['_class'] = t.__name__
131 d['_module'] = t.__module__
132 return simplejson.dumps(d)
133
134 def to_json(self):
135 """Creating a JSON representation of an instance of Credentials.
136
137 Returns:
138 string, a JSON representation of this instance, suitable to pass to
139 from_json().
140 """
141 return self._to_json(Credentials.NON_SERIALIZED_MEMBERS)
142
143 @classmethod
144 def new_from_json(cls, s):
145 """Utility class method to instantiate a Credentials subclass from a JSON
146 representation produced by to_json().
147
148 Args:
149 s: string, JSON from to_json().
150
151 Returns:
152 An instance of the subclass of Credentials that was serialized with
153 to_json().
154 """
155 data = simplejson.loads(s)
156 # Find and call the right classmethod from_json() to restore the object.
157 module = data['_module']
Joe Gregorioe9b40f12011-10-13 10:03:28 -0400158 m = __import__(module, fromlist=module.split('.')[:-1])
Joe Gregorio562b7312011-09-15 09:06:38 -0400159 kls = getattr(m, data['_class'])
160 from_json = getattr(kls, 'from_json')
161 return from_json(s)
162
JacobMoshenko8e905102011-06-20 09:53:10 -0400163
Joe Gregorio695fdc12011-01-16 16:46:55 -0500164class Flow(object):
165 """Base class for all Flow objects."""
166 pass
167
168
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500169class Storage(object):
170 """Base class for all Storage objects.
171
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400172 Store and retrieve a single credential. This class supports locking
173 such that multiple processes and threads can operate on a single
174 store.
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500175 """
176
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400177 def acquire_lock(self):
178 """Acquires any lock necessary to access this Storage.
179
180 This lock is not reentrant."""
181 pass
182
183 def release_lock(self):
184 """Release the Storage lock.
185
186 Trying to release a lock that isn't held will result in a
187 RuntimeError.
188 """
189 pass
190
191 def locked_get(self):
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500192 """Retrieve credential.
193
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400194 The Storage lock must be held when this is called.
195
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500196 Returns:
Joe Gregorio06d852b2011-03-25 15:03:10 -0400197 oauth2client.client.Credentials
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500198 """
199 _abstract()
200
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400201 def locked_put(self, credentials):
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500202 """Write a credential.
203
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400204 The Storage lock must be held when this is called.
205
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500206 Args:
207 credentials: Credentials, the credentials to store.
208 """
209 _abstract()
210
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400211 def get(self):
212 """Retrieve credential.
213
214 The Storage lock must *not* be held when this is called.
215
216 Returns:
217 oauth2client.client.Credentials
218 """
219 self.acquire_lock()
220 try:
221 return self.locked_get()
222 finally:
223 self.release_lock()
224
225 def put(self, credentials):
226 """Write a credential.
227
228 The Storage lock must be held when this is called.
229
230 Args:
231 credentials: Credentials, the credentials to store.
232 """
233 self.acquire_lock()
234 try:
235 self.locked_put(credentials)
236 finally:
237 self.release_lock()
238
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500239
Joe Gregorio695fdc12011-01-16 16:46:55 -0500240class OAuth2Credentials(Credentials):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400241 """Credentials object for OAuth 2.0.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500242
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500243 Credentials can be applied to an httplib2.Http object using the authorize()
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500244 method, which then adds the OAuth 2.0 access token to each request.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500245
246 OAuth2Credentials objects may be safely pickled and unpickled.
247 """
248
249 def __init__(self, access_token, client_id, client_secret, refresh_token,
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500250 token_expiry, token_uri, user_agent, id_token=None):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400251 """Create an instance of OAuth2Credentials.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500252
253 This constructor is not usually called by the user, instead
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500254 OAuth2Credentials objects are instantiated by the OAuth2WebServerFlow.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500255
256 Args:
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400257 access_token: string, access token.
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500258 client_id: string, client identifier.
259 client_secret: string, client secret.
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500260 refresh_token: string, refresh token.
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400261 token_expiry: datetime, when the access_token expires.
262 token_uri: string, URI of token endpoint.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500263 user_agent: string, The HTTP User-Agent to provide for this application.
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500264 id_token: object, The identity of the resource owner.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500265
Joe Gregorio695fdc12011-01-16 16:46:55 -0500266 Notes:
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500267 store: callable, A callable that when passed a Credential
Joe Gregorio695fdc12011-01-16 16:46:55 -0500268 will store the credential back to where it came from.
269 This is needed to store the latest access_token if it
270 has expired and been refreshed.
271 """
272 self.access_token = access_token
273 self.client_id = client_id
274 self.client_secret = client_secret
275 self.refresh_token = refresh_token
276 self.store = None
277 self.token_expiry = token_expiry
278 self.token_uri = token_uri
279 self.user_agent = user_agent
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500280 self.id_token = id_token
Joe Gregorio695fdc12011-01-16 16:46:55 -0500281
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500282 # True if the credentials have been revoked or expired and can't be
283 # refreshed.
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400284 self.invalid = False
Joe Gregorio9ce4b622011-02-17 15:32:11 -0500285
Joe Gregorio562b7312011-09-15 09:06:38 -0400286 def to_json(self):
287 return self._to_json(Credentials.NON_SERIALIZED_MEMBERS)
288
289 @classmethod
290 def from_json(cls, s):
291 """Instantiate a Credentials object from a JSON description of it. The JSON
292 should have been produced by calling .to_json() on the object.
293
294 Args:
295 data: dict, A deserialized JSON object.
296
297 Returns:
298 An instance of a Credentials subclass.
299 """
300 data = simplejson.loads(s)
301 if 'token_expiry' in data and not isinstance(data['token_expiry'],
302 datetime.datetime):
Joe Gregorio1daa71b2011-09-15 18:12:14 -0400303 try:
304 data['token_expiry'] = datetime.datetime.strptime(
305 data['token_expiry'], EXPIRY_FORMAT)
306 except:
307 data['token_expiry'] = None
Joe Gregorio562b7312011-09-15 09:06:38 -0400308 retval = OAuth2Credentials(
309 data['access_token'],
310 data['client_id'],
311 data['client_secret'],
312 data['refresh_token'],
313 data['token_expiry'],
314 data['token_uri'],
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500315 data['user_agent'],
316 data.get('id_token', None))
Joe Gregorio562b7312011-09-15 09:06:38 -0400317 retval.invalid = data['invalid']
318 return retval
319
Joe Gregorio9ce4b622011-02-17 15:32:11 -0500320 @property
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400321 def access_token_expired(self):
322 """True if the credential is expired or invalid.
323
324 If the token_expiry isn't set, we assume the token doesn't expire.
325 """
326 if self.invalid:
327 return True
328
329 if not self.token_expiry:
330 return False
331
Joe Gregorio562b7312011-09-15 09:06:38 -0400332 now = datetime.datetime.utcnow()
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400333 if now >= self.token_expiry:
334 logger.info('access_token is expired. Now: %s, token_expiry: %s',
335 now, self.token_expiry)
336 return True
337 return False
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500338
Joe Gregorio695fdc12011-01-16 16:46:55 -0500339 def set_store(self, store):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400340 """Set the Storage for the credential.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500341
342 Args:
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400343 store: Storage, an implementation of Stroage object.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500344 This is needed to store the latest access_token if it
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400345 has expired and been refreshed. This implementation uses
346 locking to check for updates before updating the
347 access_token.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500348 """
349 self.store = store
350
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400351 def _updateFromCredential(self, other):
352 """Update this Credential from another instance."""
353 self.__dict__.update(other.__getstate__())
354
Joe Gregorio695fdc12011-01-16 16:46:55 -0500355 def __getstate__(self):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400356 """Trim the state down to something that can be pickled."""
Joe Gregorio695fdc12011-01-16 16:46:55 -0500357 d = copy.copy(self.__dict__)
358 del d['store']
359 return d
360
361 def __setstate__(self, state):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400362 """Reconstitute the state of the object from being pickled."""
Joe Gregorio695fdc12011-01-16 16:46:55 -0500363 self.__dict__.update(state)
364 self.store = None
365
JacobMoshenko8e905102011-06-20 09:53:10 -0400366 def _generate_refresh_request_body(self):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400367 """Generate the body that will be used in the refresh request."""
JacobMoshenko8e905102011-06-20 09:53:10 -0400368 body = urllib.urlencode({
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400369 'grant_type': 'refresh_token',
370 'client_id': self.client_id,
371 'client_secret': self.client_secret,
372 'refresh_token': self.refresh_token,
373 })
JacobMoshenko8e905102011-06-20 09:53:10 -0400374 return body
375
376 def _generate_refresh_request_headers(self):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400377 """Generate the headers that will be used in the refresh request."""
JacobMoshenko8e905102011-06-20 09:53:10 -0400378 headers = {
JacobMoshenko8e905102011-06-20 09:53:10 -0400379 'content-type': 'application/x-www-form-urlencoded',
380 }
JacobMoshenkocb6d8912011-07-08 13:35:15 -0400381
382 if self.user_agent is not None:
383 headers['user-agent'] = self.user_agent
384
JacobMoshenko8e905102011-06-20 09:53:10 -0400385 return headers
386
Joe Gregorio695fdc12011-01-16 16:46:55 -0500387 def _refresh(self, http_request):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400388 """Refreshes the access_token.
389
390 This method first checks by reading the Storage object if available.
391 If a refresh is still needed, it holds the Storage lock until the
392 refresh is completed.
393 """
394 if not self.store:
395 self._do_refresh_request(http_request)
396 else:
397 self.store.acquire_lock()
398 try:
399 new_cred = self.store.locked_get()
400 if (new_cred and not new_cred.invalid and
401 new_cred.access_token != self.access_token):
402 logger.info('Updated access_token read from Storage')
403 self._updateFromCredential(new_cred)
404 else:
405 self._do_refresh_request(http_request)
406 finally:
407 self.store.release_lock()
408
409 def _do_refresh_request(self, http_request):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500410 """Refresh the access_token using the refresh_token.
411
412 Args:
413 http: An instance of httplib2.Http.request
414 or something that acts like it.
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400415
416 Raises:
417 AccessTokenRefreshError: When the refresh fails.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500418 """
JacobMoshenko8e905102011-06-20 09:53:10 -0400419 body = self._generate_refresh_request_body()
420 headers = self._generate_refresh_request_headers()
421
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400422 logger.info('Refresing access_token')
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500423 resp, content = http_request(
424 self.token_uri, method='POST', body=body, headers=headers)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500425 if resp.status == 200:
426 # TODO(jcgregorio) Raise an error if loads fails?
427 d = simplejson.loads(content)
428 self.access_token = d['access_token']
429 self.refresh_token = d.get('refresh_token', self.refresh_token)
430 if 'expires_in' in d:
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500431 self.token_expiry = datetime.timedelta(
Joe Gregorio562b7312011-09-15 09:06:38 -0400432 seconds=int(d['expires_in'])) + datetime.datetime.utcnow()
Joe Gregorio695fdc12011-01-16 16:46:55 -0500433 else:
434 self.token_expiry = None
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400435 if self.store:
436 self.store.locked_put(self)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500437 else:
JacobMoshenko8e905102011-06-20 09:53:10 -0400438 # An {'error':...} response body means the token is expired or revoked,
439 # so we flag the credentials as such.
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400440 logger.error('Failed to retrieve access token: %s' % content)
Joe Gregorioccc79542011-02-19 00:05:26 -0500441 error_msg = 'Invalid response %s.' % resp['status']
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500442 try:
443 d = simplejson.loads(content)
444 if 'error' in d:
Joe Gregorioccc79542011-02-19 00:05:26 -0500445 error_msg = d['error']
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400446 self.invalid = True
447 if self.store:
448 self.store.locked_put(self)
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500449 except:
450 pass
Joe Gregorioccc79542011-02-19 00:05:26 -0500451 raise AccessTokenRefreshError(error_msg)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500452
453 def authorize(self, http):
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500454 """Authorize an httplib2.Http instance with these credentials.
455
Joe Gregorio695fdc12011-01-16 16:46:55 -0500456 Args:
457 http: An instance of httplib2.Http
458 or something that acts like it.
459
460 Returns:
461 A modified instance of http that was passed in.
462
463 Example:
464
465 h = httplib2.Http()
466 h = credentials.authorize(h)
467
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400468 You can't create a new OAuth subclass of httplib2.Authenication
469 because it never gets passed the absolute URI, which is needed for
470 signing. So instead we have to overload 'request' with a closure
471 that adds in the Authorization header and then calls the original
472 version of 'request()'.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500473 """
474 request_orig = http.request
475
476 # The closure that will replace 'httplib2.Http.request'.
477 def new_request(uri, method='GET', body=None, headers=None,
478 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
479 connection_type=None):
JacobMoshenko8e905102011-06-20 09:53:10 -0400480 if not self.access_token:
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400481 logger.info('Attempting refresh to obtain initial access_token')
JacobMoshenko8e905102011-06-20 09:53:10 -0400482 self._refresh(request_orig)
483
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400484 # Modify the request headers to add the appropriate
485 # Authorization header.
486 if headers is None:
Joe Gregorio695fdc12011-01-16 16:46:55 -0500487 headers = {}
Joe Gregorio49e94d82011-01-28 16:36:13 -0500488 headers['authorization'] = 'OAuth ' + self.access_token
JacobMoshenkocb6d8912011-07-08 13:35:15 -0400489
490 if self.user_agent is not None:
491 if 'user-agent' in headers:
492 headers['user-agent'] = self.user_agent + ' ' + headers['user-agent']
493 else:
494 headers['user-agent'] = self.user_agent
JacobMoshenko8e905102011-06-20 09:53:10 -0400495
Joe Gregorio695fdc12011-01-16 16:46:55 -0500496 resp, content = request_orig(uri, method, body, headers,
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500497 redirections, connection_type)
JacobMoshenko8e905102011-06-20 09:53:10 -0400498
Joe Gregoriofd19cd32011-01-20 11:37:29 -0500499 if resp.status == 401:
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400500 logger.info('Refreshing due to a 401')
Joe Gregorio695fdc12011-01-16 16:46:55 -0500501 self._refresh(request_orig)
Joe Gregorioccc79542011-02-19 00:05:26 -0500502 headers['authorization'] = 'OAuth ' + self.access_token
Joe Gregorio695fdc12011-01-16 16:46:55 -0500503 return request_orig(uri, method, body, headers,
504 redirections, connection_type)
505 else:
506 return (resp, content)
507
508 http.request = new_request
509 return http
510
511
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500512class AccessTokenCredentials(OAuth2Credentials):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400513 """Credentials object for OAuth 2.0.
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500514
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400515 Credentials can be applied to an httplib2.Http object using the
516 authorize() method, which then signs each request from that object
517 with the OAuth 2.0 access token. This set of credentials is for the
518 use case where you have acquired an OAuth 2.0 access_token from
519 another place such as a JavaScript client or another web
520 application, and wish to use it from Python. Because only the
521 access_token is present it can not be refreshed and will in time
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500522 expire.
523
Joe Gregorio9ce4b622011-02-17 15:32:11 -0500524 AccessTokenCredentials objects may be safely pickled and unpickled.
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500525
526 Usage:
527 credentials = AccessTokenCredentials('<an access token>',
528 'my-user-agent/1.0')
529 http = httplib2.Http()
530 http = credentials.authorize(http)
531
532 Exceptions:
533 AccessTokenCredentialsExpired: raised when the access_token expires or is
534 revoked.
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500535 """
536
537 def __init__(self, access_token, user_agent):
538 """Create an instance of OAuth2Credentials
539
540 This is one of the few types if Credentials that you should contrust,
541 Credentials objects are usually instantiated by a Flow.
542
543 Args:
ade@google.com93a7f7c2011-02-23 16:00:37 +0000544 access_token: string, access token.
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500545 user_agent: string, The HTTP User-Agent to provide for this application.
546
547 Notes:
548 store: callable, a callable that when passed a Credential
549 will store the credential back to where it came from.
550 """
551 super(AccessTokenCredentials, self).__init__(
552 access_token,
553 None,
554 None,
555 None,
556 None,
557 None,
558 user_agent)
559
Joe Gregorio562b7312011-09-15 09:06:38 -0400560
561 @classmethod
562 def from_json(cls, s):
563 data = simplejson.loads(s)
564 retval = AccessTokenCredentials(
565 data['access_token'],
566 data['user_agent'])
567 return retval
568
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500569 def _refresh(self, http_request):
570 raise AccessTokenCredentialsError(
571 "The access_token is expired or invalid and can't be refreshed.")
572
JacobMoshenko8e905102011-06-20 09:53:10 -0400573
574class AssertionCredentials(OAuth2Credentials):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400575 """Abstract Credentials object used for OAuth 2.0 assertion grants.
JacobMoshenko8e905102011-06-20 09:53:10 -0400576
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400577 This credential does not require a flow to instantiate because it
578 represents a two legged flow, and therefore has all of the required
579 information to generate and refresh its own access tokens. It must
580 be subclassed to generate the appropriate assertion string.
JacobMoshenko8e905102011-06-20 09:53:10 -0400581
582 AssertionCredentials objects may be safely pickled and unpickled.
583 """
584
585 def __init__(self, assertion_type, user_agent,
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400586 token_uri='https://accounts.google.com/o/oauth2/token',
587 **unused_kwargs):
588 """Constructor for AssertionFlowCredentials.
JacobMoshenko8e905102011-06-20 09:53:10 -0400589
590 Args:
591 assertion_type: string, assertion type that will be declared to the auth
592 server
593 user_agent: string, The HTTP User-Agent to provide for this application.
594 token_uri: string, URI for token endpoint. For convenience
595 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
596 """
597 super(AssertionCredentials, self).__init__(
598 None,
599 None,
600 None,
601 None,
602 None,
603 token_uri,
604 user_agent)
605 self.assertion_type = assertion_type
606
607 def _generate_refresh_request_body(self):
608 assertion = self._generate_assertion()
609
610 body = urllib.urlencode({
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400611 'assertion_type': self.assertion_type,
612 'assertion': assertion,
613 'grant_type': 'assertion',
614 })
JacobMoshenko8e905102011-06-20 09:53:10 -0400615
616 return body
617
618 def _generate_assertion(self):
619 """Generate the assertion string that will be used in the access token
620 request.
621 """
622 _abstract()
623
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500624if HAS_OPENSSL:
625 # PyOpenSSL is not a prerequisite for oauth2client, so if it is missing then
626 # don't create the SignedJwtAssertionCredentials or the verify_id_token()
627 # method.
628
629 class SignedJwtAssertionCredentials(AssertionCredentials):
630 """Credentials object used for OAuth 2.0 Signed JWT assertion grants.
631
632 This credential does not require a flow to instantiate because it
633 represents a two legged flow, and therefore has all of the required
634 information to generate and refresh its own access tokens.
635 """
636
637 MAX_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds
638
639 def __init__(self,
640 service_account_name,
641 private_key,
642 scope,
643 private_key_password='notasecret',
644 user_agent=None,
645 token_uri='https://accounts.google.com/o/oauth2/token',
646 **kwargs):
647 """Constructor for SignedJwtAssertionCredentials.
648
649 Args:
650 service_account_name: string, id for account, usually an email address.
651 private_key: string, private key in P12 format.
652 scope: string or list of strings, scope(s) of the credentials being
653 requested.
654 private_key_password: string, password for private_key.
655 user_agent: string, HTTP User-Agent to provide for this application.
656 token_uri: string, URI for token endpoint. For convenience
657 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
658 kwargs: kwargs, Additional parameters to add to the JWT token, for
659 example prn=joe@xample.org."""
660
661 super(SignedJwtAssertionCredentials, self).__init__(
662 'http://oauth.net/grant_type/jwt/1.0/bearer',
663 user_agent,
664 token_uri=token_uri,
665 )
666
667 if type(scope) is list:
668 scope = ' '.join(scope)
669 self.scope = scope
670
671 self.private_key = private_key
672 self.private_key_password = private_key_password
673 self.service_account_name = service_account_name
674 self.kwargs = kwargs
675
676 @classmethod
677 def from_json(cls, s):
678 data = simplejson.loads(s)
679 retval = SignedJwtAssertionCredentials(
680 data['service_account_name'],
681 data['private_key'],
682 data['private_key_password'],
683 data['scope'],
684 data['user_agent'],
685 data['token_uri'],
686 data['kwargs']
687 )
688 retval.invalid = data['invalid']
689 return retval
690
691 def _generate_assertion(self):
692 """Generate the assertion that will be used in the request."""
693 now = long(time.time())
694 payload = {
695 'aud': self.token_uri,
696 'scope': self.scope,
697 'iat': now,
698 'exp': now + SignedJwtAssertionCredentials.MAX_TOKEN_LIFETIME_SECS,
699 'iss': self.service_account_name
700 }
701 payload.update(self.kwargs)
702 logging.debug(str(payload))
703
704 return make_signed_jwt(
705 Signer.from_string(self.private_key, self.private_key_password),
706 payload)
707
708
709 def verify_id_token(id_token, audience, http=None,
710 cert_uri=ID_TOKEN_VERIFICATON_CERTS):
711 """Verifies a signed JWT id_token.
712
713 Args:
714 id_token: string, A Signed JWT.
715 audience: string, The audience 'aud' that the token should be for.
716 http: httplib2.Http, instance to use to make the HTTP request. Callers
717 should supply an instance that has caching enabled.
718 cert_uri: string, URI of the certificates in JSON format to
719 verify the JWT against.
720
721 Returns:
722 The deserialized JSON in the JWT.
723
724 Raises:
725 oauth2client.crypt.AppIdentityError if the JWT fails to verify.
726 """
727 if http is None:
Joe Gregoriodceef692012-01-30 19:41:30 -0500728 http = httplib2.Http()
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500729
730 resp, content = http.request(cert_uri)
731
732 if resp.status == 200:
733 certs = simplejson.loads(content)
734 return verify_signed_jwt_with_certs(id_token, certs, audience)
735 else:
736 raise VerifyJwtTokenError('Status code: %d' % resp.status)
737
738
739def _urlsafe_b64decode(b64string):
Joe Gregoriobd512b52011-12-06 15:39:26 -0500740 # Guard against unicode strings, which base64 can't handle.
741 b64string = b64string.encode('ascii')
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500742 padded = b64string + '=' * (4 - len(b64string) % 4)
743 return base64.urlsafe_b64decode(padded)
744
745
746def _extract_id_token(id_token):
747 """Extract the JSON payload from a JWT.
748
749 Does the extraction w/o checking the signature.
750
751 Args:
752 id_token: string, OAuth 2.0 id_token.
753
754 Returns:
755 object, The deserialized JSON payload.
756 """
757 segments = id_token.split('.')
758
759 if (len(segments) != 3):
760 raise VerifyJwtTokenError(
761 'Wrong number of segments in token: %s' % id_token)
762
763 return simplejson.loads(_urlsafe_b64decode(segments[1]))
764
JacobMoshenko8e905102011-06-20 09:53:10 -0400765
Joe Gregorio695fdc12011-01-16 16:46:55 -0500766class OAuth2WebServerFlow(Flow):
767 """Does the Web Server Flow for OAuth 2.0.
768
769 OAuth2Credentials objects may be safely pickled and unpickled.
770 """
771
Joe Gregoriof08a4982011-10-07 13:11:16 -0400772 def __init__(self, client_id, client_secret, scope, user_agent=None,
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400773 auth_uri='https://accounts.google.com/o/oauth2/auth',
774 token_uri='https://accounts.google.com/o/oauth2/token',
775 **kwargs):
776 """Constructor for OAuth2WebServerFlow.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500777
778 Args:
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500779 client_id: string, client identifier.
780 client_secret: string client secret.
Joe Gregoriof2f8a5a2011-10-14 15:11:29 -0400781 scope: string or list of strings, scope(s) of the credentials being
782 requested.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500783 user_agent: string, HTTP User-Agent to provide for this application.
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500784 auth_uri: string, URI for authorization endpoint. For convenience
785 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
786 token_uri: string, URI for token endpoint. For convenience
787 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500788 **kwargs: dict, The keyword arguments are all optional and required
789 parameters for the OAuth calls.
790 """
791 self.client_id = client_id
792 self.client_secret = client_secret
Joe Gregoriof2f8a5a2011-10-14 15:11:29 -0400793 if type(scope) is list:
794 scope = ' '.join(scope)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500795 self.scope = scope
796 self.user_agent = user_agent
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500797 self.auth_uri = auth_uri
Joe Gregorio695fdc12011-01-16 16:46:55 -0500798 self.token_uri = token_uri
Joe Gregorio69a0aca2011-11-03 10:47:32 -0400799 self.params = {
800 'access_type': 'offline',
801 }
802 self.params.update(kwargs)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500803 self.redirect_uri = None
804
805 def step1_get_authorize_url(self, redirect_uri='oob'):
806 """Returns a URI to redirect to the provider.
807
808 Args:
809 redirect_uri: string, Either the string 'oob' for a non-web-based
810 application, or a URI that handles the callback from
811 the authorization server.
812
813 If redirect_uri is 'oob' then pass in the
814 generated verification code to step2_exchange,
815 otherwise pass in the query parameters received
816 at the callback uri to step2_exchange.
817 """
818
819 self.redirect_uri = redirect_uri
820 query = {
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400821 'response_type': 'code',
822 'client_id': self.client_id,
823 'redirect_uri': redirect_uri,
824 'scope': self.scope,
825 }
Joe Gregorio695fdc12011-01-16 16:46:55 -0500826 query.update(self.params)
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500827 parts = list(urlparse.urlparse(self.auth_uri))
Joe Gregorio695fdc12011-01-16 16:46:55 -0500828 query.update(dict(parse_qsl(parts[4]))) # 4 is the index of the query part
829 parts[4] = urllib.urlencode(query)
830 return urlparse.urlunparse(parts)
831
Joe Gregorioccc79542011-02-19 00:05:26 -0500832 def step2_exchange(self, code, http=None):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500833 """Exhanges a code for OAuth2Credentials.
834
835 Args:
836 code: string or dict, either the code as a string, or a dictionary
837 of the query parameters to the redirect_uri, which contains
838 the code.
Joe Gregorioccc79542011-02-19 00:05:26 -0500839 http: httplib2.Http, optional http instance to use to do the fetch
Joe Gregorio695fdc12011-01-16 16:46:55 -0500840 """
841
842 if not (isinstance(code, str) or isinstance(code, unicode)):
843 code = code['code']
844
845 body = urllib.urlencode({
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400846 'grant_type': 'authorization_code',
847 'client_id': self.client_id,
848 'client_secret': self.client_secret,
849 'code': code,
850 'redirect_uri': self.redirect_uri,
851 'scope': self.scope,
852 })
Joe Gregorio695fdc12011-01-16 16:46:55 -0500853 headers = {
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400854 'content-type': 'application/x-www-form-urlencoded',
Joe Gregorio695fdc12011-01-16 16:46:55 -0500855 }
JacobMoshenkocb6d8912011-07-08 13:35:15 -0400856
857 if self.user_agent is not None:
858 headers['user-agent'] = self.user_agent
859
Joe Gregorioccc79542011-02-19 00:05:26 -0500860 if http is None:
861 http = httplib2.Http()
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500862
JacobMoshenko8e905102011-06-20 09:53:10 -0400863 resp, content = http.request(self.token_uri, method='POST', body=body,
864 headers=headers)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500865 if resp.status == 200:
866 # TODO(jcgregorio) Raise an error if simplejson.loads fails?
867 d = simplejson.loads(content)
868 access_token = d['access_token']
869 refresh_token = d.get('refresh_token', None)
870 token_expiry = None
871 if 'expires_in' in d:
Joe Gregorio562b7312011-09-15 09:06:38 -0400872 token_expiry = datetime.datetime.utcnow() + datetime.timedelta(
JacobMoshenko8e905102011-06-20 09:53:10 -0400873 seconds=int(d['expires_in']))
Joe Gregorio695fdc12011-01-16 16:46:55 -0500874
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500875 if 'id_token' in d:
876 d['id_token'] = _extract_id_token(d['id_token'])
877
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400878 logger.info('Successfully retrieved access token: %s' % content)
JacobMoshenko8e905102011-06-20 09:53:10 -0400879 return OAuth2Credentials(access_token, self.client_id,
880 self.client_secret, refresh_token, token_expiry,
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500881 self.token_uri, self.user_agent,
882 id_token=d.get('id_token', None))
Joe Gregorio695fdc12011-01-16 16:46:55 -0500883 else:
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400884 logger.error('Failed to retrieve access token: %s' % content)
Joe Gregorioccc79542011-02-19 00:05:26 -0500885 error_msg = 'Invalid response %s.' % resp['status']
886 try:
887 d = simplejson.loads(content)
888 if 'error' in d:
889 error_msg = d['error']
890 except:
891 pass
892
893 raise FlowExchangeError(error_msg)
Joe Gregoriof08a4982011-10-07 13:11:16 -0400894
895def flow_from_clientsecrets(filename, scope, message=None):
896 """Create a Flow from a clientsecrets file.
897
898 Will create the right kind of Flow based on the contents of the clientsecrets
899 file or will raise InvalidClientSecretsError for unknown types of Flows.
900
901 Args:
902 filename: string, File name of client secrets.
Joe Gregoriof2f8a5a2011-10-14 15:11:29 -0400903 scope: string or list of strings, scope(s) to request.
Joe Gregoriof08a4982011-10-07 13:11:16 -0400904 message: string, A friendly string to display to the user if the
905 clientsecrets file is missing or invalid. If message is provided then
906 sys.exit will be called in the case of an error. If message in not
907 provided then clientsecrets.InvalidClientSecretsError will be raised.
908
909 Returns:
910 A Flow object.
911
912 Raises:
913 UnknownClientSecretsFlowError if the file describes an unknown kind of Flow.
914 clientsecrets.InvalidClientSecretsError if the clientsecrets file is
915 invalid.
916 """
Joe Gregorio0984ef22011-10-14 13:17:43 -0400917 try:
918 client_type, client_info = clientsecrets.loadfile(filename)
919 if client_type in [clientsecrets.TYPE_WEB, clientsecrets.TYPE_INSTALLED]:
920 return OAuth2WebServerFlow(
921 client_info['client_id'],
922 client_info['client_secret'],
923 scope,
924 None, # user_agent
925 client_info['auth_uri'],
926 client_info['token_uri'])
927 except clientsecrets.InvalidClientSecretsError:
928 if message:
929 sys.exit(message)
930 else:
931 raise
Joe Gregoriof08a4982011-10-07 13:11:16 -0400932 else:
933 raise UnknownClientSecretsFlowError(
934 'This OAuth 2.0 flow is unsupported: "%s"' * client_type)