blob: 3440c23f23861244d1ffbeadf0c743fdd55bb3df [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 Gregorio8b4c1732011-12-06 11:28:29 -050034
35HAS_OPENSSL = False
36try:
37 from oauth2client.crypt import Signer
38 from oauth2client.crypt import make_signed_jwt
39 from oauth2client.crypt import verify_signed_jwt_with_certs
40 HAS_OPENSSL = True
41except ImportError:
42 pass
43
Joe Gregorio9da2ad82011-09-11 14:04:44 -040044try: # pragma: no cover
Joe Gregorio695fdc12011-01-16 16:46:55 -050045 import simplejson
Joe Gregorio9da2ad82011-09-11 14:04:44 -040046except ImportError: # pragma: no cover
Joe Gregorio695fdc12011-01-16 16:46:55 -050047 try:
48 # Try to import from django, should work on App Engine
49 from django.utils import simplejson
50 except ImportError:
51 # Should work for Python2.6 and higher.
52 import json as simplejson
53
54try:
Joe Gregorio9da2ad82011-09-11 14:04:44 -040055 from urlparse import parse_qsl
Joe Gregorio695fdc12011-01-16 16:46:55 -050056except ImportError:
Joe Gregorio9da2ad82011-09-11 14:04:44 -040057 from cgi import parse_qsl
58
Joe Gregorio8b4c1732011-12-06 11:28:29 -050059# Determine if we can write to the file system, and if we can use a local file
60# cache behing httplib2.
61if hasattr(os, 'tempnam'):
62 # Put cache file in the director '.cache'.
63 CACHED_HTTP = httplib2.Http('.cache')
64else:
65 CACHED_HTTP = httplib2.Http()
66
Joe Gregorio9da2ad82011-09-11 14:04:44 -040067logger = logging.getLogger(__name__)
Joe Gregorio695fdc12011-01-16 16:46:55 -050068
Joe Gregorio562b7312011-09-15 09:06:38 -040069# Expiry is stored in RFC3339 UTC format
Joe Gregorio8b4c1732011-12-06 11:28:29 -050070EXPIRY_FORMAT = '%Y-%m-%dT%H:%M:%SZ'
71
72# Which certs to use to validate id_tokens received.
73ID_TOKEN_VERIFICATON_CERTS = 'https://www.googleapis.com/oauth2/v1/certs'
Joe Gregorio562b7312011-09-15 09:06:38 -040074
Joe Gregorio695fdc12011-01-16 16:46:55 -050075
76class Error(Exception):
77 """Base error for this module."""
78 pass
79
80
Joe Gregorioccc79542011-02-19 00:05:26 -050081class FlowExchangeError(Error):
Joe Gregorioca876e42011-02-22 19:39:42 -050082 """Error trying to exchange an authorization grant for an access token."""
Joe Gregorioccc79542011-02-19 00:05:26 -050083 pass
84
85
86class AccessTokenRefreshError(Error):
Joe Gregorioca876e42011-02-22 19:39:42 -050087 """Error trying to refresh an expired access token."""
Joe Gregorio695fdc12011-01-16 16:46:55 -050088 pass
89
Joe Gregoriof08a4982011-10-07 13:11:16 -040090class UnknownClientSecretsFlowError(Error):
91 """The client secrets file called for an unknown type of OAuth 2.0 flow. """
92 pass
93
Joe Gregorio695fdc12011-01-16 16:46:55 -050094
Joe Gregorio3b79fa82011-02-17 11:47:17 -050095class AccessTokenCredentialsError(Error):
96 """Having only the access_token means no refresh is possible."""
Joe Gregorio695fdc12011-01-16 16:46:55 -050097 pass
98
99
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500100class VerifyJwtTokenError(Error):
101 """Could on retrieve certificates for validation."""
102 pass
103
104
Joe Gregorio695fdc12011-01-16 16:46:55 -0500105def _abstract():
106 raise NotImplementedError('You need to override this function')
107
108
109class Credentials(object):
110 """Base class for all Credentials objects.
111
Joe Gregorio562b7312011-09-15 09:06:38 -0400112 Subclasses must define an authorize() method that applies the credentials to
113 an HTTP transport.
114
115 Subclasses must also specify a classmethod named 'from_json' that takes a JSON
116 string as input and returns an instaniated Crentials object.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500117 """
118
Joe Gregorio562b7312011-09-15 09:06:38 -0400119 NON_SERIALIZED_MEMBERS = ['store']
120
Joe Gregorio695fdc12011-01-16 16:46:55 -0500121 def authorize(self, http):
122 """Take an httplib2.Http instance (or equivalent) and
123 authorizes it for the set of credentials, usually by
124 replacing http.request() with a method that adds in
125 the appropriate headers and then delegates to the original
126 Http.request() method.
127 """
128 _abstract()
129
Joe Gregorio562b7312011-09-15 09:06:38 -0400130 def _to_json(self, strip):
131 """Utility function for creating a JSON representation of an instance of Credentials.
132
133 Args:
134 strip: array, An array of names of members to not include in the JSON.
135
136 Returns:
137 string, a JSON representation of this instance, suitable to pass to
138 from_json().
139 """
140 t = type(self)
141 d = copy.copy(self.__dict__)
142 for member in strip:
143 del d[member]
144 if 'token_expiry' in d and isinstance(d['token_expiry'], datetime.datetime):
145 d['token_expiry'] = d['token_expiry'].strftime(EXPIRY_FORMAT)
146 # Add in information we will need later to reconsistitue this instance.
147 d['_class'] = t.__name__
148 d['_module'] = t.__module__
149 return simplejson.dumps(d)
150
151 def to_json(self):
152 """Creating a JSON representation of an instance of Credentials.
153
154 Returns:
155 string, a JSON representation of this instance, suitable to pass to
156 from_json().
157 """
158 return self._to_json(Credentials.NON_SERIALIZED_MEMBERS)
159
160 @classmethod
161 def new_from_json(cls, s):
162 """Utility class method to instantiate a Credentials subclass from a JSON
163 representation produced by to_json().
164
165 Args:
166 s: string, JSON from to_json().
167
168 Returns:
169 An instance of the subclass of Credentials that was serialized with
170 to_json().
171 """
172 data = simplejson.loads(s)
173 # Find and call the right classmethod from_json() to restore the object.
174 module = data['_module']
Joe Gregorioe9b40f12011-10-13 10:03:28 -0400175 m = __import__(module, fromlist=module.split('.')[:-1])
Joe Gregorio562b7312011-09-15 09:06:38 -0400176 kls = getattr(m, data['_class'])
177 from_json = getattr(kls, 'from_json')
178 return from_json(s)
179
JacobMoshenko8e905102011-06-20 09:53:10 -0400180
Joe Gregorio695fdc12011-01-16 16:46:55 -0500181class Flow(object):
182 """Base class for all Flow objects."""
183 pass
184
185
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500186class Storage(object):
187 """Base class for all Storage objects.
188
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400189 Store and retrieve a single credential. This class supports locking
190 such that multiple processes and threads can operate on a single
191 store.
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500192 """
193
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400194 def acquire_lock(self):
195 """Acquires any lock necessary to access this Storage.
196
197 This lock is not reentrant."""
198 pass
199
200 def release_lock(self):
201 """Release the Storage lock.
202
203 Trying to release a lock that isn't held will result in a
204 RuntimeError.
205 """
206 pass
207
208 def locked_get(self):
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500209 """Retrieve credential.
210
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400211 The Storage lock must be held when this is called.
212
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500213 Returns:
Joe Gregorio06d852b2011-03-25 15:03:10 -0400214 oauth2client.client.Credentials
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500215 """
216 _abstract()
217
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400218 def locked_put(self, credentials):
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500219 """Write a credential.
220
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400221 The Storage lock must be held when this is called.
222
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500223 Args:
224 credentials: Credentials, the credentials to store.
225 """
226 _abstract()
227
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400228 def get(self):
229 """Retrieve credential.
230
231 The Storage lock must *not* be held when this is called.
232
233 Returns:
234 oauth2client.client.Credentials
235 """
236 self.acquire_lock()
237 try:
238 return self.locked_get()
239 finally:
240 self.release_lock()
241
242 def put(self, credentials):
243 """Write a credential.
244
245 The Storage lock must be held when this is called.
246
247 Args:
248 credentials: Credentials, the credentials to store.
249 """
250 self.acquire_lock()
251 try:
252 self.locked_put(credentials)
253 finally:
254 self.release_lock()
255
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500256
Joe Gregorio695fdc12011-01-16 16:46:55 -0500257class OAuth2Credentials(Credentials):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400258 """Credentials object for OAuth 2.0.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500259
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500260 Credentials can be applied to an httplib2.Http object using the authorize()
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500261 method, which then adds the OAuth 2.0 access token to each request.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500262
263 OAuth2Credentials objects may be safely pickled and unpickled.
264 """
265
266 def __init__(self, access_token, client_id, client_secret, refresh_token,
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500267 token_expiry, token_uri, user_agent, id_token=None):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400268 """Create an instance of OAuth2Credentials.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500269
270 This constructor is not usually called by the user, instead
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500271 OAuth2Credentials objects are instantiated by the OAuth2WebServerFlow.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500272
273 Args:
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400274 access_token: string, access token.
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500275 client_id: string, client identifier.
276 client_secret: string, client secret.
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500277 refresh_token: string, refresh token.
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400278 token_expiry: datetime, when the access_token expires.
279 token_uri: string, URI of token endpoint.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500280 user_agent: string, The HTTP User-Agent to provide for this application.
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500281 id_token: object, The identity of the resource owner.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500282
Joe Gregorio695fdc12011-01-16 16:46:55 -0500283 Notes:
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500284 store: callable, A callable that when passed a Credential
Joe Gregorio695fdc12011-01-16 16:46:55 -0500285 will store the credential back to where it came from.
286 This is needed to store the latest access_token if it
287 has expired and been refreshed.
288 """
289 self.access_token = access_token
290 self.client_id = client_id
291 self.client_secret = client_secret
292 self.refresh_token = refresh_token
293 self.store = None
294 self.token_expiry = token_expiry
295 self.token_uri = token_uri
296 self.user_agent = user_agent
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500297 self.id_token = id_token
Joe Gregorio695fdc12011-01-16 16:46:55 -0500298
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500299 # True if the credentials have been revoked or expired and can't be
300 # refreshed.
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400301 self.invalid = False
Joe Gregorio9ce4b622011-02-17 15:32:11 -0500302
Joe Gregorio562b7312011-09-15 09:06:38 -0400303 def to_json(self):
304 return self._to_json(Credentials.NON_SERIALIZED_MEMBERS)
305
306 @classmethod
307 def from_json(cls, s):
308 """Instantiate a Credentials object from a JSON description of it. The JSON
309 should have been produced by calling .to_json() on the object.
310
311 Args:
312 data: dict, A deserialized JSON object.
313
314 Returns:
315 An instance of a Credentials subclass.
316 """
317 data = simplejson.loads(s)
318 if 'token_expiry' in data and not isinstance(data['token_expiry'],
319 datetime.datetime):
Joe Gregorio1daa71b2011-09-15 18:12:14 -0400320 try:
321 data['token_expiry'] = datetime.datetime.strptime(
322 data['token_expiry'], EXPIRY_FORMAT)
323 except:
324 data['token_expiry'] = None
Joe Gregorio562b7312011-09-15 09:06:38 -0400325 retval = OAuth2Credentials(
326 data['access_token'],
327 data['client_id'],
328 data['client_secret'],
329 data['refresh_token'],
330 data['token_expiry'],
331 data['token_uri'],
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500332 data['user_agent'],
333 data.get('id_token', None))
Joe Gregorio562b7312011-09-15 09:06:38 -0400334 retval.invalid = data['invalid']
335 return retval
336
Joe Gregorio9ce4b622011-02-17 15:32:11 -0500337 @property
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400338 def access_token_expired(self):
339 """True if the credential is expired or invalid.
340
341 If the token_expiry isn't set, we assume the token doesn't expire.
342 """
343 if self.invalid:
344 return True
345
346 if not self.token_expiry:
347 return False
348
Joe Gregorio562b7312011-09-15 09:06:38 -0400349 now = datetime.datetime.utcnow()
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400350 if now >= self.token_expiry:
351 logger.info('access_token is expired. Now: %s, token_expiry: %s',
352 now, self.token_expiry)
353 return True
354 return False
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500355
Joe Gregorio695fdc12011-01-16 16:46:55 -0500356 def set_store(self, store):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400357 """Set the Storage for the credential.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500358
359 Args:
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400360 store: Storage, an implementation of Stroage object.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500361 This is needed to store the latest access_token if it
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400362 has expired and been refreshed. This implementation uses
363 locking to check for updates before updating the
364 access_token.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500365 """
366 self.store = store
367
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400368 def _updateFromCredential(self, other):
369 """Update this Credential from another instance."""
370 self.__dict__.update(other.__getstate__())
371
Joe Gregorio695fdc12011-01-16 16:46:55 -0500372 def __getstate__(self):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400373 """Trim the state down to something that can be pickled."""
Joe Gregorio695fdc12011-01-16 16:46:55 -0500374 d = copy.copy(self.__dict__)
375 del d['store']
376 return d
377
378 def __setstate__(self, state):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400379 """Reconstitute the state of the object from being pickled."""
Joe Gregorio695fdc12011-01-16 16:46:55 -0500380 self.__dict__.update(state)
381 self.store = None
382
JacobMoshenko8e905102011-06-20 09:53:10 -0400383 def _generate_refresh_request_body(self):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400384 """Generate the body that will be used in the refresh request."""
JacobMoshenko8e905102011-06-20 09:53:10 -0400385 body = urllib.urlencode({
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400386 'grant_type': 'refresh_token',
387 'client_id': self.client_id,
388 'client_secret': self.client_secret,
389 'refresh_token': self.refresh_token,
390 })
JacobMoshenko8e905102011-06-20 09:53:10 -0400391 return body
392
393 def _generate_refresh_request_headers(self):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400394 """Generate the headers that will be used in the refresh request."""
JacobMoshenko8e905102011-06-20 09:53:10 -0400395 headers = {
JacobMoshenko8e905102011-06-20 09:53:10 -0400396 'content-type': 'application/x-www-form-urlencoded',
397 }
JacobMoshenkocb6d8912011-07-08 13:35:15 -0400398
399 if self.user_agent is not None:
400 headers['user-agent'] = self.user_agent
401
JacobMoshenko8e905102011-06-20 09:53:10 -0400402 return headers
403
Joe Gregorio695fdc12011-01-16 16:46:55 -0500404 def _refresh(self, http_request):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400405 """Refreshes the access_token.
406
407 This method first checks by reading the Storage object if available.
408 If a refresh is still needed, it holds the Storage lock until the
409 refresh is completed.
410 """
411 if not self.store:
412 self._do_refresh_request(http_request)
413 else:
414 self.store.acquire_lock()
415 try:
416 new_cred = self.store.locked_get()
417 if (new_cred and not new_cred.invalid and
418 new_cred.access_token != self.access_token):
419 logger.info('Updated access_token read from Storage')
420 self._updateFromCredential(new_cred)
421 else:
422 self._do_refresh_request(http_request)
423 finally:
424 self.store.release_lock()
425
426 def _do_refresh_request(self, http_request):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500427 """Refresh the access_token using the refresh_token.
428
429 Args:
430 http: An instance of httplib2.Http.request
431 or something that acts like it.
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400432
433 Raises:
434 AccessTokenRefreshError: When the refresh fails.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500435 """
JacobMoshenko8e905102011-06-20 09:53:10 -0400436 body = self._generate_refresh_request_body()
437 headers = self._generate_refresh_request_headers()
438
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400439 logger.info('Refresing access_token')
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500440 resp, content = http_request(
441 self.token_uri, method='POST', body=body, headers=headers)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500442 if resp.status == 200:
443 # TODO(jcgregorio) Raise an error if loads fails?
444 d = simplejson.loads(content)
445 self.access_token = d['access_token']
446 self.refresh_token = d.get('refresh_token', self.refresh_token)
447 if 'expires_in' in d:
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500448 self.token_expiry = datetime.timedelta(
Joe Gregorio562b7312011-09-15 09:06:38 -0400449 seconds=int(d['expires_in'])) + datetime.datetime.utcnow()
Joe Gregorio695fdc12011-01-16 16:46:55 -0500450 else:
451 self.token_expiry = None
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400452 if self.store:
453 self.store.locked_put(self)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500454 else:
JacobMoshenko8e905102011-06-20 09:53:10 -0400455 # An {'error':...} response body means the token is expired or revoked,
456 # so we flag the credentials as such.
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400457 logger.error('Failed to retrieve access token: %s' % content)
Joe Gregorioccc79542011-02-19 00:05:26 -0500458 error_msg = 'Invalid response %s.' % resp['status']
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500459 try:
460 d = simplejson.loads(content)
461 if 'error' in d:
Joe Gregorioccc79542011-02-19 00:05:26 -0500462 error_msg = d['error']
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400463 self.invalid = True
464 if self.store:
465 self.store.locked_put(self)
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500466 except:
467 pass
Joe Gregorioccc79542011-02-19 00:05:26 -0500468 raise AccessTokenRefreshError(error_msg)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500469
470 def authorize(self, http):
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500471 """Authorize an httplib2.Http instance with these credentials.
472
Joe Gregorio695fdc12011-01-16 16:46:55 -0500473 Args:
474 http: An instance of httplib2.Http
475 or something that acts like it.
476
477 Returns:
478 A modified instance of http that was passed in.
479
480 Example:
481
482 h = httplib2.Http()
483 h = credentials.authorize(h)
484
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400485 You can't create a new OAuth subclass of httplib2.Authenication
486 because it never gets passed the absolute URI, which is needed for
487 signing. So instead we have to overload 'request' with a closure
488 that adds in the Authorization header and then calls the original
489 version of 'request()'.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500490 """
491 request_orig = http.request
492
493 # The closure that will replace 'httplib2.Http.request'.
494 def new_request(uri, method='GET', body=None, headers=None,
495 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
496 connection_type=None):
JacobMoshenko8e905102011-06-20 09:53:10 -0400497 if not self.access_token:
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400498 logger.info('Attempting refresh to obtain initial access_token')
JacobMoshenko8e905102011-06-20 09:53:10 -0400499 self._refresh(request_orig)
500
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400501 # Modify the request headers to add the appropriate
502 # Authorization header.
503 if headers is None:
Joe Gregorio695fdc12011-01-16 16:46:55 -0500504 headers = {}
Joe Gregorio49e94d82011-01-28 16:36:13 -0500505 headers['authorization'] = 'OAuth ' + self.access_token
JacobMoshenkocb6d8912011-07-08 13:35:15 -0400506
507 if self.user_agent is not None:
508 if 'user-agent' in headers:
509 headers['user-agent'] = self.user_agent + ' ' + headers['user-agent']
510 else:
511 headers['user-agent'] = self.user_agent
JacobMoshenko8e905102011-06-20 09:53:10 -0400512
Joe Gregorio695fdc12011-01-16 16:46:55 -0500513 resp, content = request_orig(uri, method, body, headers,
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500514 redirections, connection_type)
JacobMoshenko8e905102011-06-20 09:53:10 -0400515
Joe Gregoriofd19cd32011-01-20 11:37:29 -0500516 if resp.status == 401:
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400517 logger.info('Refreshing due to a 401')
Joe Gregorio695fdc12011-01-16 16:46:55 -0500518 self._refresh(request_orig)
Joe Gregorioccc79542011-02-19 00:05:26 -0500519 headers['authorization'] = 'OAuth ' + self.access_token
Joe Gregorio695fdc12011-01-16 16:46:55 -0500520 return request_orig(uri, method, body, headers,
521 redirections, connection_type)
522 else:
523 return (resp, content)
524
525 http.request = new_request
526 return http
527
528
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500529class AccessTokenCredentials(OAuth2Credentials):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400530 """Credentials object for OAuth 2.0.
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500531
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400532 Credentials can be applied to an httplib2.Http object using the
533 authorize() method, which then signs each request from that object
534 with the OAuth 2.0 access token. This set of credentials is for the
535 use case where you have acquired an OAuth 2.0 access_token from
536 another place such as a JavaScript client or another web
537 application, and wish to use it from Python. Because only the
538 access_token is present it can not be refreshed and will in time
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500539 expire.
540
Joe Gregorio9ce4b622011-02-17 15:32:11 -0500541 AccessTokenCredentials objects may be safely pickled and unpickled.
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500542
543 Usage:
544 credentials = AccessTokenCredentials('<an access token>',
545 'my-user-agent/1.0')
546 http = httplib2.Http()
547 http = credentials.authorize(http)
548
549 Exceptions:
550 AccessTokenCredentialsExpired: raised when the access_token expires or is
551 revoked.
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500552 """
553
554 def __init__(self, access_token, user_agent):
555 """Create an instance of OAuth2Credentials
556
557 This is one of the few types if Credentials that you should contrust,
558 Credentials objects are usually instantiated by a Flow.
559
560 Args:
ade@google.com93a7f7c2011-02-23 16:00:37 +0000561 access_token: string, access token.
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500562 user_agent: string, The HTTP User-Agent to provide for this application.
563
564 Notes:
565 store: callable, a callable that when passed a Credential
566 will store the credential back to where it came from.
567 """
568 super(AccessTokenCredentials, self).__init__(
569 access_token,
570 None,
571 None,
572 None,
573 None,
574 None,
575 user_agent)
576
Joe Gregorio562b7312011-09-15 09:06:38 -0400577
578 @classmethod
579 def from_json(cls, s):
580 data = simplejson.loads(s)
581 retval = AccessTokenCredentials(
582 data['access_token'],
583 data['user_agent'])
584 return retval
585
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500586 def _refresh(self, http_request):
587 raise AccessTokenCredentialsError(
588 "The access_token is expired or invalid and can't be refreshed.")
589
JacobMoshenko8e905102011-06-20 09:53:10 -0400590
591class AssertionCredentials(OAuth2Credentials):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400592 """Abstract Credentials object used for OAuth 2.0 assertion grants.
JacobMoshenko8e905102011-06-20 09:53:10 -0400593
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400594 This credential does not require a flow to instantiate because it
595 represents a two legged flow, and therefore has all of the required
596 information to generate and refresh its own access tokens. It must
597 be subclassed to generate the appropriate assertion string.
JacobMoshenko8e905102011-06-20 09:53:10 -0400598
599 AssertionCredentials objects may be safely pickled and unpickled.
600 """
601
602 def __init__(self, assertion_type, user_agent,
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400603 token_uri='https://accounts.google.com/o/oauth2/token',
604 **unused_kwargs):
605 """Constructor for AssertionFlowCredentials.
JacobMoshenko8e905102011-06-20 09:53:10 -0400606
607 Args:
608 assertion_type: string, assertion type that will be declared to the auth
609 server
610 user_agent: string, The HTTP User-Agent to provide for this application.
611 token_uri: string, URI for token endpoint. For convenience
612 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
613 """
614 super(AssertionCredentials, self).__init__(
615 None,
616 None,
617 None,
618 None,
619 None,
620 token_uri,
621 user_agent)
622 self.assertion_type = assertion_type
623
624 def _generate_refresh_request_body(self):
625 assertion = self._generate_assertion()
626
627 body = urllib.urlencode({
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400628 'assertion_type': self.assertion_type,
629 'assertion': assertion,
630 'grant_type': 'assertion',
631 })
JacobMoshenko8e905102011-06-20 09:53:10 -0400632
633 return body
634
635 def _generate_assertion(self):
636 """Generate the assertion string that will be used in the access token
637 request.
638 """
639 _abstract()
640
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500641if HAS_OPENSSL:
642 # PyOpenSSL is not a prerequisite for oauth2client, so if it is missing then
643 # don't create the SignedJwtAssertionCredentials or the verify_id_token()
644 # method.
645
646 class SignedJwtAssertionCredentials(AssertionCredentials):
647 """Credentials object used for OAuth 2.0 Signed JWT assertion grants.
648
649 This credential does not require a flow to instantiate because it
650 represents a two legged flow, and therefore has all of the required
651 information to generate and refresh its own access tokens.
652 """
653
654 MAX_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds
655
656 def __init__(self,
657 service_account_name,
658 private_key,
659 scope,
660 private_key_password='notasecret',
661 user_agent=None,
662 token_uri='https://accounts.google.com/o/oauth2/token',
663 **kwargs):
664 """Constructor for SignedJwtAssertionCredentials.
665
666 Args:
667 service_account_name: string, id for account, usually an email address.
668 private_key: string, private key in P12 format.
669 scope: string or list of strings, scope(s) of the credentials being
670 requested.
671 private_key_password: string, password for private_key.
672 user_agent: string, HTTP User-Agent to provide for this application.
673 token_uri: string, URI for token endpoint. For convenience
674 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
675 kwargs: kwargs, Additional parameters to add to the JWT token, for
676 example prn=joe@xample.org."""
677
678 super(SignedJwtAssertionCredentials, self).__init__(
679 'http://oauth.net/grant_type/jwt/1.0/bearer',
680 user_agent,
681 token_uri=token_uri,
682 )
683
684 if type(scope) is list:
685 scope = ' '.join(scope)
686 self.scope = scope
687
688 self.private_key = private_key
689 self.private_key_password = private_key_password
690 self.service_account_name = service_account_name
691 self.kwargs = kwargs
692
693 @classmethod
694 def from_json(cls, s):
695 data = simplejson.loads(s)
696 retval = SignedJwtAssertionCredentials(
697 data['service_account_name'],
698 data['private_key'],
699 data['private_key_password'],
700 data['scope'],
701 data['user_agent'],
702 data['token_uri'],
703 data['kwargs']
704 )
705 retval.invalid = data['invalid']
706 return retval
707
708 def _generate_assertion(self):
709 """Generate the assertion that will be used in the request."""
710 now = long(time.time())
711 payload = {
712 'aud': self.token_uri,
713 'scope': self.scope,
714 'iat': now,
715 'exp': now + SignedJwtAssertionCredentials.MAX_TOKEN_LIFETIME_SECS,
716 'iss': self.service_account_name
717 }
718 payload.update(self.kwargs)
719 logging.debug(str(payload))
720
721 return make_signed_jwt(
722 Signer.from_string(self.private_key, self.private_key_password),
723 payload)
724
725
726 def verify_id_token(id_token, audience, http=None,
727 cert_uri=ID_TOKEN_VERIFICATON_CERTS):
728 """Verifies a signed JWT id_token.
729
730 Args:
731 id_token: string, A Signed JWT.
732 audience: string, The audience 'aud' that the token should be for.
733 http: httplib2.Http, instance to use to make the HTTP request. Callers
734 should supply an instance that has caching enabled.
735 cert_uri: string, URI of the certificates in JSON format to
736 verify the JWT against.
737
738 Returns:
739 The deserialized JSON in the JWT.
740
741 Raises:
742 oauth2client.crypt.AppIdentityError if the JWT fails to verify.
743 """
744 if http is None:
745 http = CACHED_HTTP
746
747 resp, content = http.request(cert_uri)
748
749 if resp.status == 200:
750 certs = simplejson.loads(content)
751 return verify_signed_jwt_with_certs(id_token, certs, audience)
752 else:
753 raise VerifyJwtTokenError('Status code: %d' % resp.status)
754
755
756def _urlsafe_b64decode(b64string):
757 padded = b64string + '=' * (4 - len(b64string) % 4)
758 return base64.urlsafe_b64decode(padded)
759
760
761def _extract_id_token(id_token):
762 """Extract the JSON payload from a JWT.
763
764 Does the extraction w/o checking the signature.
765
766 Args:
767 id_token: string, OAuth 2.0 id_token.
768
769 Returns:
770 object, The deserialized JSON payload.
771 """
772 segments = id_token.split('.')
773
774 if (len(segments) != 3):
775 raise VerifyJwtTokenError(
776 'Wrong number of segments in token: %s' % id_token)
777
778 return simplejson.loads(_urlsafe_b64decode(segments[1]))
779
JacobMoshenko8e905102011-06-20 09:53:10 -0400780
Joe Gregorio695fdc12011-01-16 16:46:55 -0500781class OAuth2WebServerFlow(Flow):
782 """Does the Web Server Flow for OAuth 2.0.
783
784 OAuth2Credentials objects may be safely pickled and unpickled.
785 """
786
Joe Gregoriof08a4982011-10-07 13:11:16 -0400787 def __init__(self, client_id, client_secret, scope, user_agent=None,
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400788 auth_uri='https://accounts.google.com/o/oauth2/auth',
789 token_uri='https://accounts.google.com/o/oauth2/token',
790 **kwargs):
791 """Constructor for OAuth2WebServerFlow.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500792
793 Args:
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500794 client_id: string, client identifier.
795 client_secret: string client secret.
Joe Gregoriof2f8a5a2011-10-14 15:11:29 -0400796 scope: string or list of strings, scope(s) of the credentials being
797 requested.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500798 user_agent: string, HTTP User-Agent to provide for this application.
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500799 auth_uri: string, URI for authorization endpoint. For convenience
800 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
801 token_uri: string, URI for token endpoint. For convenience
802 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500803 **kwargs: dict, The keyword arguments are all optional and required
804 parameters for the OAuth calls.
805 """
806 self.client_id = client_id
807 self.client_secret = client_secret
Joe Gregoriof2f8a5a2011-10-14 15:11:29 -0400808 if type(scope) is list:
809 scope = ' '.join(scope)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500810 self.scope = scope
811 self.user_agent = user_agent
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500812 self.auth_uri = auth_uri
Joe Gregorio695fdc12011-01-16 16:46:55 -0500813 self.token_uri = token_uri
Joe Gregorio69a0aca2011-11-03 10:47:32 -0400814 self.params = {
815 'access_type': 'offline',
816 }
817 self.params.update(kwargs)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500818 self.redirect_uri = None
819
820 def step1_get_authorize_url(self, redirect_uri='oob'):
821 """Returns a URI to redirect to the provider.
822
823 Args:
824 redirect_uri: string, Either the string 'oob' for a non-web-based
825 application, or a URI that handles the callback from
826 the authorization server.
827
828 If redirect_uri is 'oob' then pass in the
829 generated verification code to step2_exchange,
830 otherwise pass in the query parameters received
831 at the callback uri to step2_exchange.
832 """
833
834 self.redirect_uri = redirect_uri
835 query = {
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400836 'response_type': 'code',
837 'client_id': self.client_id,
838 'redirect_uri': redirect_uri,
839 'scope': self.scope,
840 }
Joe Gregorio695fdc12011-01-16 16:46:55 -0500841 query.update(self.params)
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500842 parts = list(urlparse.urlparse(self.auth_uri))
Joe Gregorio695fdc12011-01-16 16:46:55 -0500843 query.update(dict(parse_qsl(parts[4]))) # 4 is the index of the query part
844 parts[4] = urllib.urlencode(query)
845 return urlparse.urlunparse(parts)
846
Joe Gregorioccc79542011-02-19 00:05:26 -0500847 def step2_exchange(self, code, http=None):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500848 """Exhanges a code for OAuth2Credentials.
849
850 Args:
851 code: string or dict, either the code as a string, or a dictionary
852 of the query parameters to the redirect_uri, which contains
853 the code.
Joe Gregorioccc79542011-02-19 00:05:26 -0500854 http: httplib2.Http, optional http instance to use to do the fetch
Joe Gregorio695fdc12011-01-16 16:46:55 -0500855 """
856
857 if not (isinstance(code, str) or isinstance(code, unicode)):
858 code = code['code']
859
860 body = urllib.urlencode({
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400861 'grant_type': 'authorization_code',
862 'client_id': self.client_id,
863 'client_secret': self.client_secret,
864 'code': code,
865 'redirect_uri': self.redirect_uri,
866 'scope': self.scope,
867 })
Joe Gregorio695fdc12011-01-16 16:46:55 -0500868 headers = {
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400869 'content-type': 'application/x-www-form-urlencoded',
Joe Gregorio695fdc12011-01-16 16:46:55 -0500870 }
JacobMoshenkocb6d8912011-07-08 13:35:15 -0400871
872 if self.user_agent is not None:
873 headers['user-agent'] = self.user_agent
874
Joe Gregorioccc79542011-02-19 00:05:26 -0500875 if http is None:
876 http = httplib2.Http()
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500877
JacobMoshenko8e905102011-06-20 09:53:10 -0400878 resp, content = http.request(self.token_uri, method='POST', body=body,
879 headers=headers)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500880 if resp.status == 200:
881 # TODO(jcgregorio) Raise an error if simplejson.loads fails?
882 d = simplejson.loads(content)
883 access_token = d['access_token']
884 refresh_token = d.get('refresh_token', None)
885 token_expiry = None
886 if 'expires_in' in d:
Joe Gregorio562b7312011-09-15 09:06:38 -0400887 token_expiry = datetime.datetime.utcnow() + datetime.timedelta(
JacobMoshenko8e905102011-06-20 09:53:10 -0400888 seconds=int(d['expires_in']))
Joe Gregorio695fdc12011-01-16 16:46:55 -0500889
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500890 if 'id_token' in d:
891 d['id_token'] = _extract_id_token(d['id_token'])
892
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400893 logger.info('Successfully retrieved access token: %s' % content)
JacobMoshenko8e905102011-06-20 09:53:10 -0400894 return OAuth2Credentials(access_token, self.client_id,
895 self.client_secret, refresh_token, token_expiry,
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500896 self.token_uri, self.user_agent,
897 id_token=d.get('id_token', None))
Joe Gregorio695fdc12011-01-16 16:46:55 -0500898 else:
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400899 logger.error('Failed to retrieve access token: %s' % content)
Joe Gregorioccc79542011-02-19 00:05:26 -0500900 error_msg = 'Invalid response %s.' % resp['status']
901 try:
902 d = simplejson.loads(content)
903 if 'error' in d:
904 error_msg = d['error']
905 except:
906 pass
907
908 raise FlowExchangeError(error_msg)
Joe Gregoriof08a4982011-10-07 13:11:16 -0400909
910def flow_from_clientsecrets(filename, scope, message=None):
911 """Create a Flow from a clientsecrets file.
912
913 Will create the right kind of Flow based on the contents of the clientsecrets
914 file or will raise InvalidClientSecretsError for unknown types of Flows.
915
916 Args:
917 filename: string, File name of client secrets.
Joe Gregoriof2f8a5a2011-10-14 15:11:29 -0400918 scope: string or list of strings, scope(s) to request.
Joe Gregoriof08a4982011-10-07 13:11:16 -0400919 message: string, A friendly string to display to the user if the
920 clientsecrets file is missing or invalid. If message is provided then
921 sys.exit will be called in the case of an error. If message in not
922 provided then clientsecrets.InvalidClientSecretsError will be raised.
923
924 Returns:
925 A Flow object.
926
927 Raises:
928 UnknownClientSecretsFlowError if the file describes an unknown kind of Flow.
929 clientsecrets.InvalidClientSecretsError if the clientsecrets file is
930 invalid.
931 """
Joe Gregorio0984ef22011-10-14 13:17:43 -0400932 try:
933 client_type, client_info = clientsecrets.loadfile(filename)
934 if client_type in [clientsecrets.TYPE_WEB, clientsecrets.TYPE_INSTALLED]:
935 return OAuth2WebServerFlow(
936 client_info['client_id'],
937 client_info['client_secret'],
938 scope,
939 None, # user_agent
940 client_info['auth_uri'],
941 client_info['token_uri'])
942 except clientsecrets.InvalidClientSecretsError:
943 if message:
944 sys.exit(message)
945 else:
946 raise
Joe Gregoriof08a4982011-10-07 13:11:16 -0400947 else:
948 raise UnknownClientSecretsFlowError(
949 'This OAuth 2.0 flow is unsupported: "%s"' * client_type)