blob: 46d6cff2a92c0af7486e93b75bfc719ef1f78739 [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
Joe Gregorio8b4c1732011-12-06 11:28:29 -050050# Determine if we can write to the file system, and if we can use a local file
51# cache behing httplib2.
52if hasattr(os, 'tempnam'):
53 # Put cache file in the director '.cache'.
54 CACHED_HTTP = httplib2.Http('.cache')
55else:
56 CACHED_HTTP = httplib2.Http()
57
Joe Gregorio9da2ad82011-09-11 14:04:44 -040058logger = logging.getLogger(__name__)
Joe Gregorio695fdc12011-01-16 16:46:55 -050059
Joe Gregorio562b7312011-09-15 09:06:38 -040060# Expiry is stored in RFC3339 UTC format
Joe Gregorio8b4c1732011-12-06 11:28:29 -050061EXPIRY_FORMAT = '%Y-%m-%dT%H:%M:%SZ'
62
63# Which certs to use to validate id_tokens received.
64ID_TOKEN_VERIFICATON_CERTS = 'https://www.googleapis.com/oauth2/v1/certs'
Joe Gregorio562b7312011-09-15 09:06:38 -040065
Joe Gregorio695fdc12011-01-16 16:46:55 -050066
67class Error(Exception):
68 """Base error for this module."""
69 pass
70
71
Joe Gregorioccc79542011-02-19 00:05:26 -050072class FlowExchangeError(Error):
Joe Gregorioca876e42011-02-22 19:39:42 -050073 """Error trying to exchange an authorization grant for an access token."""
Joe Gregorioccc79542011-02-19 00:05:26 -050074 pass
75
76
77class AccessTokenRefreshError(Error):
Joe Gregorioca876e42011-02-22 19:39:42 -050078 """Error trying to refresh an expired access token."""
Joe Gregorio695fdc12011-01-16 16:46:55 -050079 pass
80
Joe Gregoriof08a4982011-10-07 13:11:16 -040081class UnknownClientSecretsFlowError(Error):
82 """The client secrets file called for an unknown type of OAuth 2.0 flow. """
83 pass
84
Joe Gregorio695fdc12011-01-16 16:46:55 -050085
Joe Gregorio3b79fa82011-02-17 11:47:17 -050086class AccessTokenCredentialsError(Error):
87 """Having only the access_token means no refresh is possible."""
Joe Gregorio695fdc12011-01-16 16:46:55 -050088 pass
89
90
Joe Gregorio8b4c1732011-12-06 11:28:29 -050091class VerifyJwtTokenError(Error):
92 """Could on retrieve certificates for validation."""
93 pass
94
95
Joe Gregorio695fdc12011-01-16 16:46:55 -050096def _abstract():
97 raise NotImplementedError('You need to override this function')
98
99
100class Credentials(object):
101 """Base class for all Credentials objects.
102
Joe Gregorio562b7312011-09-15 09:06:38 -0400103 Subclasses must define an authorize() method that applies the credentials to
104 an HTTP transport.
105
106 Subclasses must also specify a classmethod named 'from_json' that takes a JSON
107 string as input and returns an instaniated Crentials object.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500108 """
109
Joe Gregorio562b7312011-09-15 09:06:38 -0400110 NON_SERIALIZED_MEMBERS = ['store']
111
Joe Gregorio695fdc12011-01-16 16:46:55 -0500112 def authorize(self, http):
113 """Take an httplib2.Http instance (or equivalent) and
114 authorizes it for the set of credentials, usually by
115 replacing http.request() with a method that adds in
116 the appropriate headers and then delegates to the original
117 Http.request() method.
118 """
119 _abstract()
120
Joe Gregorio562b7312011-09-15 09:06:38 -0400121 def _to_json(self, strip):
122 """Utility function for creating a JSON representation of an instance of Credentials.
123
124 Args:
125 strip: array, An array of names of members to not include in the JSON.
126
127 Returns:
128 string, a JSON representation of this instance, suitable to pass to
129 from_json().
130 """
131 t = type(self)
132 d = copy.copy(self.__dict__)
133 for member in strip:
134 del d[member]
135 if 'token_expiry' in d and isinstance(d['token_expiry'], datetime.datetime):
136 d['token_expiry'] = d['token_expiry'].strftime(EXPIRY_FORMAT)
137 # Add in information we will need later to reconsistitue this instance.
138 d['_class'] = t.__name__
139 d['_module'] = t.__module__
140 return simplejson.dumps(d)
141
142 def to_json(self):
143 """Creating a JSON representation of an instance of Credentials.
144
145 Returns:
146 string, a JSON representation of this instance, suitable to pass to
147 from_json().
148 """
149 return self._to_json(Credentials.NON_SERIALIZED_MEMBERS)
150
151 @classmethod
152 def new_from_json(cls, s):
153 """Utility class method to instantiate a Credentials subclass from a JSON
154 representation produced by to_json().
155
156 Args:
157 s: string, JSON from to_json().
158
159 Returns:
160 An instance of the subclass of Credentials that was serialized with
161 to_json().
162 """
163 data = simplejson.loads(s)
164 # Find and call the right classmethod from_json() to restore the object.
165 module = data['_module']
Joe Gregorioe9b40f12011-10-13 10:03:28 -0400166 m = __import__(module, fromlist=module.split('.')[:-1])
Joe Gregorio562b7312011-09-15 09:06:38 -0400167 kls = getattr(m, data['_class'])
168 from_json = getattr(kls, 'from_json')
169 return from_json(s)
170
JacobMoshenko8e905102011-06-20 09:53:10 -0400171
Joe Gregorio695fdc12011-01-16 16:46:55 -0500172class Flow(object):
173 """Base class for all Flow objects."""
174 pass
175
176
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500177class Storage(object):
178 """Base class for all Storage objects.
179
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400180 Store and retrieve a single credential. This class supports locking
181 such that multiple processes and threads can operate on a single
182 store.
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500183 """
184
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400185 def acquire_lock(self):
186 """Acquires any lock necessary to access this Storage.
187
188 This lock is not reentrant."""
189 pass
190
191 def release_lock(self):
192 """Release the Storage lock.
193
194 Trying to release a lock that isn't held will result in a
195 RuntimeError.
196 """
197 pass
198
199 def locked_get(self):
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500200 """Retrieve credential.
201
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400202 The Storage lock must be held when this is called.
203
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500204 Returns:
Joe Gregorio06d852b2011-03-25 15:03:10 -0400205 oauth2client.client.Credentials
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500206 """
207 _abstract()
208
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400209 def locked_put(self, credentials):
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500210 """Write a credential.
211
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400212 The Storage lock must be held when this is called.
213
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500214 Args:
215 credentials: Credentials, the credentials to store.
216 """
217 _abstract()
218
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400219 def get(self):
220 """Retrieve credential.
221
222 The Storage lock must *not* be held when this is called.
223
224 Returns:
225 oauth2client.client.Credentials
226 """
227 self.acquire_lock()
228 try:
229 return self.locked_get()
230 finally:
231 self.release_lock()
232
233 def put(self, credentials):
234 """Write a credential.
235
236 The Storage lock must be held when this is called.
237
238 Args:
239 credentials: Credentials, the credentials to store.
240 """
241 self.acquire_lock()
242 try:
243 self.locked_put(credentials)
244 finally:
245 self.release_lock()
246
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500247
Joe Gregorio695fdc12011-01-16 16:46:55 -0500248class OAuth2Credentials(Credentials):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400249 """Credentials object for OAuth 2.0.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500250
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500251 Credentials can be applied to an httplib2.Http object using the authorize()
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500252 method, which then adds the OAuth 2.0 access token to each request.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500253
254 OAuth2Credentials objects may be safely pickled and unpickled.
255 """
256
257 def __init__(self, access_token, client_id, client_secret, refresh_token,
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500258 token_expiry, token_uri, user_agent, id_token=None):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400259 """Create an instance of OAuth2Credentials.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500260
261 This constructor is not usually called by the user, instead
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500262 OAuth2Credentials objects are instantiated by the OAuth2WebServerFlow.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500263
264 Args:
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400265 access_token: string, access token.
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500266 client_id: string, client identifier.
267 client_secret: string, client secret.
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500268 refresh_token: string, refresh token.
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400269 token_expiry: datetime, when the access_token expires.
270 token_uri: string, URI of token endpoint.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500271 user_agent: string, The HTTP User-Agent to provide for this application.
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500272 id_token: object, The identity of the resource owner.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500273
Joe Gregorio695fdc12011-01-16 16:46:55 -0500274 Notes:
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500275 store: callable, A callable that when passed a Credential
Joe Gregorio695fdc12011-01-16 16:46:55 -0500276 will store the credential back to where it came from.
277 This is needed to store the latest access_token if it
278 has expired and been refreshed.
279 """
280 self.access_token = access_token
281 self.client_id = client_id
282 self.client_secret = client_secret
283 self.refresh_token = refresh_token
284 self.store = None
285 self.token_expiry = token_expiry
286 self.token_uri = token_uri
287 self.user_agent = user_agent
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500288 self.id_token = id_token
Joe Gregorio695fdc12011-01-16 16:46:55 -0500289
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500290 # True if the credentials have been revoked or expired and can't be
291 # refreshed.
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400292 self.invalid = False
Joe Gregorio9ce4b622011-02-17 15:32:11 -0500293
Joe Gregorio562b7312011-09-15 09:06:38 -0400294 def to_json(self):
295 return self._to_json(Credentials.NON_SERIALIZED_MEMBERS)
296
297 @classmethod
298 def from_json(cls, s):
299 """Instantiate a Credentials object from a JSON description of it. The JSON
300 should have been produced by calling .to_json() on the object.
301
302 Args:
303 data: dict, A deserialized JSON object.
304
305 Returns:
306 An instance of a Credentials subclass.
307 """
308 data = simplejson.loads(s)
309 if 'token_expiry' in data and not isinstance(data['token_expiry'],
310 datetime.datetime):
Joe Gregorio1daa71b2011-09-15 18:12:14 -0400311 try:
312 data['token_expiry'] = datetime.datetime.strptime(
313 data['token_expiry'], EXPIRY_FORMAT)
314 except:
315 data['token_expiry'] = None
Joe Gregorio562b7312011-09-15 09:06:38 -0400316 retval = OAuth2Credentials(
317 data['access_token'],
318 data['client_id'],
319 data['client_secret'],
320 data['refresh_token'],
321 data['token_expiry'],
322 data['token_uri'],
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500323 data['user_agent'],
324 data.get('id_token', None))
Joe Gregorio562b7312011-09-15 09:06:38 -0400325 retval.invalid = data['invalid']
326 return retval
327
Joe Gregorio9ce4b622011-02-17 15:32:11 -0500328 @property
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400329 def access_token_expired(self):
330 """True if the credential is expired or invalid.
331
332 If the token_expiry isn't set, we assume the token doesn't expire.
333 """
334 if self.invalid:
335 return True
336
337 if not self.token_expiry:
338 return False
339
Joe Gregorio562b7312011-09-15 09:06:38 -0400340 now = datetime.datetime.utcnow()
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400341 if now >= self.token_expiry:
342 logger.info('access_token is expired. Now: %s, token_expiry: %s',
343 now, self.token_expiry)
344 return True
345 return False
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500346
Joe Gregorio695fdc12011-01-16 16:46:55 -0500347 def set_store(self, store):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400348 """Set the Storage for the credential.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500349
350 Args:
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400351 store: Storage, an implementation of Stroage object.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500352 This is needed to store the latest access_token if it
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400353 has expired and been refreshed. This implementation uses
354 locking to check for updates before updating the
355 access_token.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500356 """
357 self.store = store
358
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400359 def _updateFromCredential(self, other):
360 """Update this Credential from another instance."""
361 self.__dict__.update(other.__getstate__())
362
Joe Gregorio695fdc12011-01-16 16:46:55 -0500363 def __getstate__(self):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400364 """Trim the state down to something that can be pickled."""
Joe Gregorio695fdc12011-01-16 16:46:55 -0500365 d = copy.copy(self.__dict__)
366 del d['store']
367 return d
368
369 def __setstate__(self, state):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400370 """Reconstitute the state of the object from being pickled."""
Joe Gregorio695fdc12011-01-16 16:46:55 -0500371 self.__dict__.update(state)
372 self.store = None
373
JacobMoshenko8e905102011-06-20 09:53:10 -0400374 def _generate_refresh_request_body(self):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400375 """Generate the body that will be used in the refresh request."""
JacobMoshenko8e905102011-06-20 09:53:10 -0400376 body = urllib.urlencode({
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400377 'grant_type': 'refresh_token',
378 'client_id': self.client_id,
379 'client_secret': self.client_secret,
380 'refresh_token': self.refresh_token,
381 })
JacobMoshenko8e905102011-06-20 09:53:10 -0400382 return body
383
384 def _generate_refresh_request_headers(self):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400385 """Generate the headers that will be used in the refresh request."""
JacobMoshenko8e905102011-06-20 09:53:10 -0400386 headers = {
JacobMoshenko8e905102011-06-20 09:53:10 -0400387 'content-type': 'application/x-www-form-urlencoded',
388 }
JacobMoshenkocb6d8912011-07-08 13:35:15 -0400389
390 if self.user_agent is not None:
391 headers['user-agent'] = self.user_agent
392
JacobMoshenko8e905102011-06-20 09:53:10 -0400393 return headers
394
Joe Gregorio695fdc12011-01-16 16:46:55 -0500395 def _refresh(self, http_request):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400396 """Refreshes the access_token.
397
398 This method first checks by reading the Storage object if available.
399 If a refresh is still needed, it holds the Storage lock until the
400 refresh is completed.
401 """
402 if not self.store:
403 self._do_refresh_request(http_request)
404 else:
405 self.store.acquire_lock()
406 try:
407 new_cred = self.store.locked_get()
408 if (new_cred and not new_cred.invalid and
409 new_cred.access_token != self.access_token):
410 logger.info('Updated access_token read from Storage')
411 self._updateFromCredential(new_cred)
412 else:
413 self._do_refresh_request(http_request)
414 finally:
415 self.store.release_lock()
416
417 def _do_refresh_request(self, http_request):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500418 """Refresh the access_token using the refresh_token.
419
420 Args:
421 http: An instance of httplib2.Http.request
422 or something that acts like it.
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400423
424 Raises:
425 AccessTokenRefreshError: When the refresh fails.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500426 """
JacobMoshenko8e905102011-06-20 09:53:10 -0400427 body = self._generate_refresh_request_body()
428 headers = self._generate_refresh_request_headers()
429
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400430 logger.info('Refresing access_token')
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500431 resp, content = http_request(
432 self.token_uri, method='POST', body=body, headers=headers)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500433 if resp.status == 200:
434 # TODO(jcgregorio) Raise an error if loads fails?
435 d = simplejson.loads(content)
436 self.access_token = d['access_token']
437 self.refresh_token = d.get('refresh_token', self.refresh_token)
438 if 'expires_in' in d:
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500439 self.token_expiry = datetime.timedelta(
Joe Gregorio562b7312011-09-15 09:06:38 -0400440 seconds=int(d['expires_in'])) + datetime.datetime.utcnow()
Joe Gregorio695fdc12011-01-16 16:46:55 -0500441 else:
442 self.token_expiry = None
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400443 if self.store:
444 self.store.locked_put(self)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500445 else:
JacobMoshenko8e905102011-06-20 09:53:10 -0400446 # An {'error':...} response body means the token is expired or revoked,
447 # so we flag the credentials as such.
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400448 logger.error('Failed to retrieve access token: %s' % content)
Joe Gregorioccc79542011-02-19 00:05:26 -0500449 error_msg = 'Invalid response %s.' % resp['status']
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500450 try:
451 d = simplejson.loads(content)
452 if 'error' in d:
Joe Gregorioccc79542011-02-19 00:05:26 -0500453 error_msg = d['error']
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400454 self.invalid = True
455 if self.store:
456 self.store.locked_put(self)
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500457 except:
458 pass
Joe Gregorioccc79542011-02-19 00:05:26 -0500459 raise AccessTokenRefreshError(error_msg)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500460
461 def authorize(self, http):
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500462 """Authorize an httplib2.Http instance with these credentials.
463
Joe Gregorio695fdc12011-01-16 16:46:55 -0500464 Args:
465 http: An instance of httplib2.Http
466 or something that acts like it.
467
468 Returns:
469 A modified instance of http that was passed in.
470
471 Example:
472
473 h = httplib2.Http()
474 h = credentials.authorize(h)
475
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400476 You can't create a new OAuth subclass of httplib2.Authenication
477 because it never gets passed the absolute URI, which is needed for
478 signing. So instead we have to overload 'request' with a closure
479 that adds in the Authorization header and then calls the original
480 version of 'request()'.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500481 """
482 request_orig = http.request
483
484 # The closure that will replace 'httplib2.Http.request'.
485 def new_request(uri, method='GET', body=None, headers=None,
486 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
487 connection_type=None):
JacobMoshenko8e905102011-06-20 09:53:10 -0400488 if not self.access_token:
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400489 logger.info('Attempting refresh to obtain initial access_token')
JacobMoshenko8e905102011-06-20 09:53:10 -0400490 self._refresh(request_orig)
491
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400492 # Modify the request headers to add the appropriate
493 # Authorization header.
494 if headers is None:
Joe Gregorio695fdc12011-01-16 16:46:55 -0500495 headers = {}
Joe Gregorio49e94d82011-01-28 16:36:13 -0500496 headers['authorization'] = 'OAuth ' + self.access_token
JacobMoshenkocb6d8912011-07-08 13:35:15 -0400497
498 if self.user_agent is not None:
499 if 'user-agent' in headers:
500 headers['user-agent'] = self.user_agent + ' ' + headers['user-agent']
501 else:
502 headers['user-agent'] = self.user_agent
JacobMoshenko8e905102011-06-20 09:53:10 -0400503
Joe Gregorio695fdc12011-01-16 16:46:55 -0500504 resp, content = request_orig(uri, method, body, headers,
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500505 redirections, connection_type)
JacobMoshenko8e905102011-06-20 09:53:10 -0400506
Joe Gregoriofd19cd32011-01-20 11:37:29 -0500507 if resp.status == 401:
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400508 logger.info('Refreshing due to a 401')
Joe Gregorio695fdc12011-01-16 16:46:55 -0500509 self._refresh(request_orig)
Joe Gregorioccc79542011-02-19 00:05:26 -0500510 headers['authorization'] = 'OAuth ' + self.access_token
Joe Gregorio695fdc12011-01-16 16:46:55 -0500511 return request_orig(uri, method, body, headers,
512 redirections, connection_type)
513 else:
514 return (resp, content)
515
516 http.request = new_request
517 return http
518
519
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500520class AccessTokenCredentials(OAuth2Credentials):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400521 """Credentials object for OAuth 2.0.
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500522
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400523 Credentials can be applied to an httplib2.Http object using the
524 authorize() method, which then signs each request from that object
525 with the OAuth 2.0 access token. This set of credentials is for the
526 use case where you have acquired an OAuth 2.0 access_token from
527 another place such as a JavaScript client or another web
528 application, and wish to use it from Python. Because only the
529 access_token is present it can not be refreshed and will in time
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500530 expire.
531
Joe Gregorio9ce4b622011-02-17 15:32:11 -0500532 AccessTokenCredentials objects may be safely pickled and unpickled.
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500533
534 Usage:
535 credentials = AccessTokenCredentials('<an access token>',
536 'my-user-agent/1.0')
537 http = httplib2.Http()
538 http = credentials.authorize(http)
539
540 Exceptions:
541 AccessTokenCredentialsExpired: raised when the access_token expires or is
542 revoked.
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500543 """
544
545 def __init__(self, access_token, user_agent):
546 """Create an instance of OAuth2Credentials
547
548 This is one of the few types if Credentials that you should contrust,
549 Credentials objects are usually instantiated by a Flow.
550
551 Args:
ade@google.com93a7f7c2011-02-23 16:00:37 +0000552 access_token: string, access token.
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500553 user_agent: string, The HTTP User-Agent to provide for this application.
554
555 Notes:
556 store: callable, a callable that when passed a Credential
557 will store the credential back to where it came from.
558 """
559 super(AccessTokenCredentials, self).__init__(
560 access_token,
561 None,
562 None,
563 None,
564 None,
565 None,
566 user_agent)
567
Joe Gregorio562b7312011-09-15 09:06:38 -0400568
569 @classmethod
570 def from_json(cls, s):
571 data = simplejson.loads(s)
572 retval = AccessTokenCredentials(
573 data['access_token'],
574 data['user_agent'])
575 return retval
576
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500577 def _refresh(self, http_request):
578 raise AccessTokenCredentialsError(
579 "The access_token is expired or invalid and can't be refreshed.")
580
JacobMoshenko8e905102011-06-20 09:53:10 -0400581
582class AssertionCredentials(OAuth2Credentials):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400583 """Abstract Credentials object used for OAuth 2.0 assertion grants.
JacobMoshenko8e905102011-06-20 09:53:10 -0400584
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400585 This credential does not require a flow to instantiate because it
586 represents a two legged flow, and therefore has all of the required
587 information to generate and refresh its own access tokens. It must
588 be subclassed to generate the appropriate assertion string.
JacobMoshenko8e905102011-06-20 09:53:10 -0400589
590 AssertionCredentials objects may be safely pickled and unpickled.
591 """
592
593 def __init__(self, assertion_type, user_agent,
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400594 token_uri='https://accounts.google.com/o/oauth2/token',
595 **unused_kwargs):
596 """Constructor for AssertionFlowCredentials.
JacobMoshenko8e905102011-06-20 09:53:10 -0400597
598 Args:
599 assertion_type: string, assertion type that will be declared to the auth
600 server
601 user_agent: string, The HTTP User-Agent to provide for this application.
602 token_uri: string, URI for token endpoint. For convenience
603 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
604 """
605 super(AssertionCredentials, self).__init__(
606 None,
607 None,
608 None,
609 None,
610 None,
611 token_uri,
612 user_agent)
613 self.assertion_type = assertion_type
614
615 def _generate_refresh_request_body(self):
616 assertion = self._generate_assertion()
617
618 body = urllib.urlencode({
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400619 'assertion_type': self.assertion_type,
620 'assertion': assertion,
621 'grant_type': 'assertion',
622 })
JacobMoshenko8e905102011-06-20 09:53:10 -0400623
624 return body
625
626 def _generate_assertion(self):
627 """Generate the assertion string that will be used in the access token
628 request.
629 """
630 _abstract()
631
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500632if HAS_OPENSSL:
633 # PyOpenSSL is not a prerequisite for oauth2client, so if it is missing then
634 # don't create the SignedJwtAssertionCredentials or the verify_id_token()
635 # method.
636
637 class SignedJwtAssertionCredentials(AssertionCredentials):
638 """Credentials object used for OAuth 2.0 Signed JWT assertion grants.
639
640 This credential does not require a flow to instantiate because it
641 represents a two legged flow, and therefore has all of the required
642 information to generate and refresh its own access tokens.
643 """
644
645 MAX_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds
646
647 def __init__(self,
648 service_account_name,
649 private_key,
650 scope,
651 private_key_password='notasecret',
652 user_agent=None,
653 token_uri='https://accounts.google.com/o/oauth2/token',
654 **kwargs):
655 """Constructor for SignedJwtAssertionCredentials.
656
657 Args:
658 service_account_name: string, id for account, usually an email address.
659 private_key: string, private key in P12 format.
660 scope: string or list of strings, scope(s) of the credentials being
661 requested.
662 private_key_password: string, password for private_key.
663 user_agent: string, HTTP User-Agent to provide for this application.
664 token_uri: string, URI for token endpoint. For convenience
665 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
666 kwargs: kwargs, Additional parameters to add to the JWT token, for
667 example prn=joe@xample.org."""
668
669 super(SignedJwtAssertionCredentials, self).__init__(
670 'http://oauth.net/grant_type/jwt/1.0/bearer',
671 user_agent,
672 token_uri=token_uri,
673 )
674
675 if type(scope) is list:
676 scope = ' '.join(scope)
677 self.scope = scope
678
679 self.private_key = private_key
680 self.private_key_password = private_key_password
681 self.service_account_name = service_account_name
682 self.kwargs = kwargs
683
684 @classmethod
685 def from_json(cls, s):
686 data = simplejson.loads(s)
687 retval = SignedJwtAssertionCredentials(
688 data['service_account_name'],
689 data['private_key'],
690 data['private_key_password'],
691 data['scope'],
692 data['user_agent'],
693 data['token_uri'],
694 data['kwargs']
695 )
696 retval.invalid = data['invalid']
697 return retval
698
699 def _generate_assertion(self):
700 """Generate the assertion that will be used in the request."""
701 now = long(time.time())
702 payload = {
703 'aud': self.token_uri,
704 'scope': self.scope,
705 'iat': now,
706 'exp': now + SignedJwtAssertionCredentials.MAX_TOKEN_LIFETIME_SECS,
707 'iss': self.service_account_name
708 }
709 payload.update(self.kwargs)
710 logging.debug(str(payload))
711
712 return make_signed_jwt(
713 Signer.from_string(self.private_key, self.private_key_password),
714 payload)
715
716
717 def verify_id_token(id_token, audience, http=None,
718 cert_uri=ID_TOKEN_VERIFICATON_CERTS):
719 """Verifies a signed JWT id_token.
720
721 Args:
722 id_token: string, A Signed JWT.
723 audience: string, The audience 'aud' that the token should be for.
724 http: httplib2.Http, instance to use to make the HTTP request. Callers
725 should supply an instance that has caching enabled.
726 cert_uri: string, URI of the certificates in JSON format to
727 verify the JWT against.
728
729 Returns:
730 The deserialized JSON in the JWT.
731
732 Raises:
733 oauth2client.crypt.AppIdentityError if the JWT fails to verify.
734 """
735 if http is None:
736 http = CACHED_HTTP
737
738 resp, content = http.request(cert_uri)
739
740 if resp.status == 200:
741 certs = simplejson.loads(content)
742 return verify_signed_jwt_with_certs(id_token, certs, audience)
743 else:
744 raise VerifyJwtTokenError('Status code: %d' % resp.status)
745
746
747def _urlsafe_b64decode(b64string):
Joe Gregoriobd512b52011-12-06 15:39:26 -0500748 # Guard against unicode strings, which base64 can't handle.
749 b64string = b64string.encode('ascii')
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500750 padded = b64string + '=' * (4 - len(b64string) % 4)
751 return base64.urlsafe_b64decode(padded)
752
753
754def _extract_id_token(id_token):
755 """Extract the JSON payload from a JWT.
756
757 Does the extraction w/o checking the signature.
758
759 Args:
760 id_token: string, OAuth 2.0 id_token.
761
762 Returns:
763 object, The deserialized JSON payload.
764 """
765 segments = id_token.split('.')
766
767 if (len(segments) != 3):
768 raise VerifyJwtTokenError(
769 'Wrong number of segments in token: %s' % id_token)
770
771 return simplejson.loads(_urlsafe_b64decode(segments[1]))
772
JacobMoshenko8e905102011-06-20 09:53:10 -0400773
Joe Gregorio695fdc12011-01-16 16:46:55 -0500774class OAuth2WebServerFlow(Flow):
775 """Does the Web Server Flow for OAuth 2.0.
776
777 OAuth2Credentials objects may be safely pickled and unpickled.
778 """
779
Joe Gregoriof08a4982011-10-07 13:11:16 -0400780 def __init__(self, client_id, client_secret, scope, user_agent=None,
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400781 auth_uri='https://accounts.google.com/o/oauth2/auth',
782 token_uri='https://accounts.google.com/o/oauth2/token',
783 **kwargs):
784 """Constructor for OAuth2WebServerFlow.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500785
786 Args:
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500787 client_id: string, client identifier.
788 client_secret: string client secret.
Joe Gregoriof2f8a5a2011-10-14 15:11:29 -0400789 scope: string or list of strings, scope(s) of the credentials being
790 requested.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500791 user_agent: string, HTTP User-Agent to provide for this application.
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500792 auth_uri: string, URI for authorization endpoint. For convenience
793 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
794 token_uri: string, URI for token endpoint. For convenience
795 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500796 **kwargs: dict, The keyword arguments are all optional and required
797 parameters for the OAuth calls.
798 """
799 self.client_id = client_id
800 self.client_secret = client_secret
Joe Gregoriof2f8a5a2011-10-14 15:11:29 -0400801 if type(scope) is list:
802 scope = ' '.join(scope)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500803 self.scope = scope
804 self.user_agent = user_agent
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500805 self.auth_uri = auth_uri
Joe Gregorio695fdc12011-01-16 16:46:55 -0500806 self.token_uri = token_uri
Joe Gregorio69a0aca2011-11-03 10:47:32 -0400807 self.params = {
808 'access_type': 'offline',
809 }
810 self.params.update(kwargs)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500811 self.redirect_uri = None
812
813 def step1_get_authorize_url(self, redirect_uri='oob'):
814 """Returns a URI to redirect to the provider.
815
816 Args:
817 redirect_uri: string, Either the string 'oob' for a non-web-based
818 application, or a URI that handles the callback from
819 the authorization server.
820
821 If redirect_uri is 'oob' then pass in the
822 generated verification code to step2_exchange,
823 otherwise pass in the query parameters received
824 at the callback uri to step2_exchange.
825 """
826
827 self.redirect_uri = redirect_uri
828 query = {
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400829 'response_type': 'code',
830 'client_id': self.client_id,
831 'redirect_uri': redirect_uri,
832 'scope': self.scope,
833 }
Joe Gregorio695fdc12011-01-16 16:46:55 -0500834 query.update(self.params)
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500835 parts = list(urlparse.urlparse(self.auth_uri))
Joe Gregorio695fdc12011-01-16 16:46:55 -0500836 query.update(dict(parse_qsl(parts[4]))) # 4 is the index of the query part
837 parts[4] = urllib.urlencode(query)
838 return urlparse.urlunparse(parts)
839
Joe Gregorioccc79542011-02-19 00:05:26 -0500840 def step2_exchange(self, code, http=None):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500841 """Exhanges a code for OAuth2Credentials.
842
843 Args:
844 code: string or dict, either the code as a string, or a dictionary
845 of the query parameters to the redirect_uri, which contains
846 the code.
Joe Gregorioccc79542011-02-19 00:05:26 -0500847 http: httplib2.Http, optional http instance to use to do the fetch
Joe Gregorio695fdc12011-01-16 16:46:55 -0500848 """
849
850 if not (isinstance(code, str) or isinstance(code, unicode)):
851 code = code['code']
852
853 body = urllib.urlencode({
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400854 'grant_type': 'authorization_code',
855 'client_id': self.client_id,
856 'client_secret': self.client_secret,
857 'code': code,
858 'redirect_uri': self.redirect_uri,
859 'scope': self.scope,
860 })
Joe Gregorio695fdc12011-01-16 16:46:55 -0500861 headers = {
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400862 'content-type': 'application/x-www-form-urlencoded',
Joe Gregorio695fdc12011-01-16 16:46:55 -0500863 }
JacobMoshenkocb6d8912011-07-08 13:35:15 -0400864
865 if self.user_agent is not None:
866 headers['user-agent'] = self.user_agent
867
Joe Gregorioccc79542011-02-19 00:05:26 -0500868 if http is None:
869 http = httplib2.Http()
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500870
JacobMoshenko8e905102011-06-20 09:53:10 -0400871 resp, content = http.request(self.token_uri, method='POST', body=body,
872 headers=headers)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500873 if resp.status == 200:
874 # TODO(jcgregorio) Raise an error if simplejson.loads fails?
875 d = simplejson.loads(content)
876 access_token = d['access_token']
877 refresh_token = d.get('refresh_token', None)
878 token_expiry = None
879 if 'expires_in' in d:
Joe Gregorio562b7312011-09-15 09:06:38 -0400880 token_expiry = datetime.datetime.utcnow() + datetime.timedelta(
JacobMoshenko8e905102011-06-20 09:53:10 -0400881 seconds=int(d['expires_in']))
Joe Gregorio695fdc12011-01-16 16:46:55 -0500882
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500883 if 'id_token' in d:
884 d['id_token'] = _extract_id_token(d['id_token'])
885
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400886 logger.info('Successfully retrieved access token: %s' % content)
JacobMoshenko8e905102011-06-20 09:53:10 -0400887 return OAuth2Credentials(access_token, self.client_id,
888 self.client_secret, refresh_token, token_expiry,
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500889 self.token_uri, self.user_agent,
890 id_token=d.get('id_token', None))
Joe Gregorio695fdc12011-01-16 16:46:55 -0500891 else:
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400892 logger.error('Failed to retrieve access token: %s' % content)
Joe Gregorioccc79542011-02-19 00:05:26 -0500893 error_msg = 'Invalid response %s.' % resp['status']
894 try:
895 d = simplejson.loads(content)
896 if 'error' in d:
897 error_msg = d['error']
898 except:
899 pass
900
901 raise FlowExchangeError(error_msg)
Joe Gregoriof08a4982011-10-07 13:11:16 -0400902
903def flow_from_clientsecrets(filename, scope, message=None):
904 """Create a Flow from a clientsecrets file.
905
906 Will create the right kind of Flow based on the contents of the clientsecrets
907 file or will raise InvalidClientSecretsError for unknown types of Flows.
908
909 Args:
910 filename: string, File name of client secrets.
Joe Gregoriof2f8a5a2011-10-14 15:11:29 -0400911 scope: string or list of strings, scope(s) to request.
Joe Gregoriof08a4982011-10-07 13:11:16 -0400912 message: string, A friendly string to display to the user if the
913 clientsecrets file is missing or invalid. If message is provided then
914 sys.exit will be called in the case of an error. If message in not
915 provided then clientsecrets.InvalidClientSecretsError will be raised.
916
917 Returns:
918 A Flow object.
919
920 Raises:
921 UnknownClientSecretsFlowError if the file describes an unknown kind of Flow.
922 clientsecrets.InvalidClientSecretsError if the clientsecrets file is
923 invalid.
924 """
Joe Gregorio0984ef22011-10-14 13:17:43 -0400925 try:
926 client_type, client_info = clientsecrets.loadfile(filename)
927 if client_type in [clientsecrets.TYPE_WEB, clientsecrets.TYPE_INSTALLED]:
928 return OAuth2WebServerFlow(
929 client_info['client_id'],
930 client_info['client_secret'],
931 scope,
932 None, # user_agent
933 client_info['auth_uri'],
934 client_info['token_uri'])
935 except clientsecrets.InvalidClientSecretsError:
936 if message:
937 sys.exit(message)
938 else:
939 raise
Joe Gregoriof08a4982011-10-07 13:11:16 -0400940 else:
941 raise UnknownClientSecretsFlowError(
942 'This OAuth 2.0 flow is unsupported: "%s"' * client_type)