blob: c88b358739e96a405e886648678a256789fd0f40 [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 Gregoriof2326c02012-02-09 12:18:44 -050058# Constant to use for the out of band OAuth 2.0 flow.
59OOB_CALLBACK_URN = 'urn:ietf:wg:oauth:2.0:oob'
60
Joe Gregorio695fdc12011-01-16 16:46:55 -050061
62class Error(Exception):
63 """Base error for this module."""
64 pass
65
66
Joe Gregorioccc79542011-02-19 00:05:26 -050067class FlowExchangeError(Error):
Joe Gregorioca876e42011-02-22 19:39:42 -050068 """Error trying to exchange an authorization grant for an access token."""
Joe Gregorioccc79542011-02-19 00:05:26 -050069 pass
70
71
72class AccessTokenRefreshError(Error):
Joe Gregorioca876e42011-02-22 19:39:42 -050073 """Error trying to refresh an expired access token."""
Joe Gregorio695fdc12011-01-16 16:46:55 -050074 pass
75
Joe Gregoriof08a4982011-10-07 13:11:16 -040076class UnknownClientSecretsFlowError(Error):
77 """The client secrets file called for an unknown type of OAuth 2.0 flow. """
78 pass
79
Joe Gregorio695fdc12011-01-16 16:46:55 -050080
Joe Gregorio3b79fa82011-02-17 11:47:17 -050081class AccessTokenCredentialsError(Error):
82 """Having only the access_token means no refresh is possible."""
Joe Gregorio695fdc12011-01-16 16:46:55 -050083 pass
84
85
Joe Gregorio8b4c1732011-12-06 11:28:29 -050086class VerifyJwtTokenError(Error):
87 """Could on retrieve certificates for validation."""
88 pass
89
90
Joe Gregorio695fdc12011-01-16 16:46:55 -050091def _abstract():
92 raise NotImplementedError('You need to override this function')
93
94
Joe Gregorio9f2f38f2012-02-06 12:53:00 -050095class MemoryCache(object):
96 """httplib2 Cache implementation which only caches locally."""
97
98 def __init__(self):
99 self.cache = {}
100
101 def get(self, key):
102 return self.cache.get(key)
103
104 def set(self, key, value):
105 self.cache[key] = value
106
107 def delete(self, key):
108 self.cache.pop(key, None)
109
110
Joe Gregorio695fdc12011-01-16 16:46:55 -0500111class Credentials(object):
112 """Base class for all Credentials objects.
113
Joe Gregorio562b7312011-09-15 09:06:38 -0400114 Subclasses must define an authorize() method that applies the credentials to
115 an HTTP transport.
116
117 Subclasses must also specify a classmethod named 'from_json' that takes a JSON
118 string as input and returns an instaniated Crentials object.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500119 """
120
Joe Gregorio562b7312011-09-15 09:06:38 -0400121 NON_SERIALIZED_MEMBERS = ['store']
122
Joe Gregorio695fdc12011-01-16 16:46:55 -0500123 def authorize(self, http):
124 """Take an httplib2.Http instance (or equivalent) and
125 authorizes it for the set of credentials, usually by
126 replacing http.request() with a method that adds in
127 the appropriate headers and then delegates to the original
128 Http.request() method.
129 """
130 _abstract()
131
Joe Gregorio562b7312011-09-15 09:06:38 -0400132 def _to_json(self, strip):
133 """Utility function for creating a JSON representation of an instance of Credentials.
134
135 Args:
136 strip: array, An array of names of members to not include in the JSON.
137
138 Returns:
139 string, a JSON representation of this instance, suitable to pass to
140 from_json().
141 """
142 t = type(self)
143 d = copy.copy(self.__dict__)
144 for member in strip:
145 del d[member]
146 if 'token_expiry' in d and isinstance(d['token_expiry'], datetime.datetime):
147 d['token_expiry'] = d['token_expiry'].strftime(EXPIRY_FORMAT)
148 # Add in information we will need later to reconsistitue this instance.
149 d['_class'] = t.__name__
150 d['_module'] = t.__module__
151 return simplejson.dumps(d)
152
153 def to_json(self):
154 """Creating a JSON representation of an instance of Credentials.
155
156 Returns:
157 string, a JSON representation of this instance, suitable to pass to
158 from_json().
159 """
160 return self._to_json(Credentials.NON_SERIALIZED_MEMBERS)
161
162 @classmethod
163 def new_from_json(cls, s):
164 """Utility class method to instantiate a Credentials subclass from a JSON
165 representation produced by to_json().
166
167 Args:
168 s: string, JSON from to_json().
169
170 Returns:
171 An instance of the subclass of Credentials that was serialized with
172 to_json().
173 """
174 data = simplejson.loads(s)
175 # Find and call the right classmethod from_json() to restore the object.
176 module = data['_module']
Joe Gregorioe9b40f12011-10-13 10:03:28 -0400177 m = __import__(module, fromlist=module.split('.')[:-1])
Joe Gregorio562b7312011-09-15 09:06:38 -0400178 kls = getattr(m, data['_class'])
179 from_json = getattr(kls, 'from_json')
180 return from_json(s)
181
JacobMoshenko8e905102011-06-20 09:53:10 -0400182
Joe Gregorio695fdc12011-01-16 16:46:55 -0500183class Flow(object):
184 """Base class for all Flow objects."""
185 pass
186
187
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500188class Storage(object):
189 """Base class for all Storage objects.
190
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400191 Store and retrieve a single credential. This class supports locking
192 such that multiple processes and threads can operate on a single
193 store.
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500194 """
195
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400196 def acquire_lock(self):
197 """Acquires any lock necessary to access this Storage.
198
199 This lock is not reentrant."""
200 pass
201
202 def release_lock(self):
203 """Release the Storage lock.
204
205 Trying to release a lock that isn't held will result in a
206 RuntimeError.
207 """
208 pass
209
210 def locked_get(self):
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500211 """Retrieve credential.
212
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400213 The Storage lock must be held when this is called.
214
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500215 Returns:
Joe Gregorio06d852b2011-03-25 15:03:10 -0400216 oauth2client.client.Credentials
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500217 """
218 _abstract()
219
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400220 def locked_put(self, credentials):
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500221 """Write a credential.
222
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400223 The Storage lock must be held when this is called.
224
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500225 Args:
226 credentials: Credentials, the credentials to store.
227 """
228 _abstract()
229
Joe Gregorioec75dc12012-02-06 13:40:42 -0500230 def locked_delete(self):
231 """Delete a credential.
232
233 The Storage lock must be held when this is called.
234 """
235 _abstract()
236
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400237 def get(self):
238 """Retrieve credential.
239
240 The Storage lock must *not* be held when this is called.
241
242 Returns:
243 oauth2client.client.Credentials
244 """
245 self.acquire_lock()
246 try:
247 return self.locked_get()
248 finally:
249 self.release_lock()
250
251 def put(self, credentials):
252 """Write a credential.
253
254 The Storage lock must be held when this is called.
255
256 Args:
257 credentials: Credentials, the credentials to store.
258 """
259 self.acquire_lock()
260 try:
261 self.locked_put(credentials)
262 finally:
263 self.release_lock()
264
Joe Gregorioec75dc12012-02-06 13:40:42 -0500265 def delete(self):
266 """Delete credential.
267
268 Frees any resources associated with storing the credential.
269 The Storage lock must *not* be held when this is called.
270
271 Returns:
272 None
273 """
274 self.acquire_lock()
275 try:
276 return self.locked_delete()
277 finally:
278 self.release_lock()
279
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500280
Joe Gregorio695fdc12011-01-16 16:46:55 -0500281class OAuth2Credentials(Credentials):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400282 """Credentials object for OAuth 2.0.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500283
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500284 Credentials can be applied to an httplib2.Http object using the authorize()
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500285 method, which then adds the OAuth 2.0 access token to each request.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500286
287 OAuth2Credentials objects may be safely pickled and unpickled.
288 """
289
290 def __init__(self, access_token, client_id, client_secret, refresh_token,
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500291 token_expiry, token_uri, user_agent, id_token=None):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400292 """Create an instance of OAuth2Credentials.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500293
294 This constructor is not usually called by the user, instead
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500295 OAuth2Credentials objects are instantiated by the OAuth2WebServerFlow.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500296
297 Args:
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400298 access_token: string, access token.
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500299 client_id: string, client identifier.
300 client_secret: string, client secret.
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500301 refresh_token: string, refresh token.
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400302 token_expiry: datetime, when the access_token expires.
303 token_uri: string, URI of token endpoint.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500304 user_agent: string, The HTTP User-Agent to provide for this application.
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500305 id_token: object, The identity of the resource owner.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500306
Joe Gregorio695fdc12011-01-16 16:46:55 -0500307 Notes:
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500308 store: callable, A callable that when passed a Credential
Joe Gregorio695fdc12011-01-16 16:46:55 -0500309 will store the credential back to where it came from.
310 This is needed to store the latest access_token if it
311 has expired and been refreshed.
312 """
313 self.access_token = access_token
314 self.client_id = client_id
315 self.client_secret = client_secret
316 self.refresh_token = refresh_token
317 self.store = None
318 self.token_expiry = token_expiry
319 self.token_uri = token_uri
320 self.user_agent = user_agent
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500321 self.id_token = id_token
Joe Gregorio695fdc12011-01-16 16:46:55 -0500322
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500323 # True if the credentials have been revoked or expired and can't be
324 # refreshed.
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400325 self.invalid = False
Joe Gregorio9ce4b622011-02-17 15:32:11 -0500326
Joe Gregorio562b7312011-09-15 09:06:38 -0400327 def to_json(self):
328 return self._to_json(Credentials.NON_SERIALIZED_MEMBERS)
329
330 @classmethod
331 def from_json(cls, s):
332 """Instantiate a Credentials object from a JSON description of it. The JSON
333 should have been produced by calling .to_json() on the object.
334
335 Args:
336 data: dict, A deserialized JSON object.
337
338 Returns:
339 An instance of a Credentials subclass.
340 """
341 data = simplejson.loads(s)
342 if 'token_expiry' in data and not isinstance(data['token_expiry'],
343 datetime.datetime):
Joe Gregorio1daa71b2011-09-15 18:12:14 -0400344 try:
345 data['token_expiry'] = datetime.datetime.strptime(
346 data['token_expiry'], EXPIRY_FORMAT)
347 except:
348 data['token_expiry'] = None
Joe Gregorio562b7312011-09-15 09:06:38 -0400349 retval = OAuth2Credentials(
350 data['access_token'],
351 data['client_id'],
352 data['client_secret'],
353 data['refresh_token'],
354 data['token_expiry'],
355 data['token_uri'],
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500356 data['user_agent'],
357 data.get('id_token', None))
Joe Gregorio562b7312011-09-15 09:06:38 -0400358 retval.invalid = data['invalid']
359 return retval
360
Joe Gregorio9ce4b622011-02-17 15:32:11 -0500361 @property
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400362 def access_token_expired(self):
363 """True if the credential is expired or invalid.
364
365 If the token_expiry isn't set, we assume the token doesn't expire.
366 """
367 if self.invalid:
368 return True
369
370 if not self.token_expiry:
371 return False
372
Joe Gregorio562b7312011-09-15 09:06:38 -0400373 now = datetime.datetime.utcnow()
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400374 if now >= self.token_expiry:
375 logger.info('access_token is expired. Now: %s, token_expiry: %s',
376 now, self.token_expiry)
377 return True
378 return False
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500379
Joe Gregorio695fdc12011-01-16 16:46:55 -0500380 def set_store(self, store):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400381 """Set the Storage for the credential.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500382
383 Args:
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400384 store: Storage, an implementation of Stroage object.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500385 This is needed to store the latest access_token if it
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400386 has expired and been refreshed. This implementation uses
387 locking to check for updates before updating the
388 access_token.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500389 """
390 self.store = store
391
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400392 def _updateFromCredential(self, other):
393 """Update this Credential from another instance."""
394 self.__dict__.update(other.__getstate__())
395
Joe Gregorio695fdc12011-01-16 16:46:55 -0500396 def __getstate__(self):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400397 """Trim the state down to something that can be pickled."""
Joe Gregorio695fdc12011-01-16 16:46:55 -0500398 d = copy.copy(self.__dict__)
399 del d['store']
400 return d
401
402 def __setstate__(self, state):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400403 """Reconstitute the state of the object from being pickled."""
Joe Gregorio695fdc12011-01-16 16:46:55 -0500404 self.__dict__.update(state)
405 self.store = None
406
JacobMoshenko8e905102011-06-20 09:53:10 -0400407 def _generate_refresh_request_body(self):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400408 """Generate the body that will be used in the refresh request."""
JacobMoshenko8e905102011-06-20 09:53:10 -0400409 body = urllib.urlencode({
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400410 'grant_type': 'refresh_token',
411 'client_id': self.client_id,
412 'client_secret': self.client_secret,
413 'refresh_token': self.refresh_token,
414 })
JacobMoshenko8e905102011-06-20 09:53:10 -0400415 return body
416
417 def _generate_refresh_request_headers(self):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400418 """Generate the headers that will be used in the refresh request."""
JacobMoshenko8e905102011-06-20 09:53:10 -0400419 headers = {
JacobMoshenko8e905102011-06-20 09:53:10 -0400420 'content-type': 'application/x-www-form-urlencoded',
421 }
JacobMoshenkocb6d8912011-07-08 13:35:15 -0400422
423 if self.user_agent is not None:
424 headers['user-agent'] = self.user_agent
425
JacobMoshenko8e905102011-06-20 09:53:10 -0400426 return headers
427
Joe Gregorio695fdc12011-01-16 16:46:55 -0500428 def _refresh(self, http_request):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400429 """Refreshes the access_token.
430
431 This method first checks by reading the Storage object if available.
432 If a refresh is still needed, it holds the Storage lock until the
433 refresh is completed.
434 """
435 if not self.store:
436 self._do_refresh_request(http_request)
437 else:
438 self.store.acquire_lock()
439 try:
440 new_cred = self.store.locked_get()
441 if (new_cred and not new_cred.invalid and
442 new_cred.access_token != self.access_token):
443 logger.info('Updated access_token read from Storage')
444 self._updateFromCredential(new_cred)
445 else:
446 self._do_refresh_request(http_request)
447 finally:
448 self.store.release_lock()
449
450 def _do_refresh_request(self, http_request):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500451 """Refresh the access_token using the refresh_token.
452
453 Args:
454 http: An instance of httplib2.Http.request
455 or something that acts like it.
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400456
457 Raises:
458 AccessTokenRefreshError: When the refresh fails.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500459 """
JacobMoshenko8e905102011-06-20 09:53:10 -0400460 body = self._generate_refresh_request_body()
461 headers = self._generate_refresh_request_headers()
462
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400463 logger.info('Refresing access_token')
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500464 resp, content = http_request(
465 self.token_uri, method='POST', body=body, headers=headers)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500466 if resp.status == 200:
467 # TODO(jcgregorio) Raise an error if loads fails?
468 d = simplejson.loads(content)
469 self.access_token = d['access_token']
470 self.refresh_token = d.get('refresh_token', self.refresh_token)
471 if 'expires_in' in d:
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500472 self.token_expiry = datetime.timedelta(
Joe Gregorio562b7312011-09-15 09:06:38 -0400473 seconds=int(d['expires_in'])) + datetime.datetime.utcnow()
Joe Gregorio695fdc12011-01-16 16:46:55 -0500474 else:
475 self.token_expiry = None
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400476 if self.store:
477 self.store.locked_put(self)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500478 else:
JacobMoshenko8e905102011-06-20 09:53:10 -0400479 # An {'error':...} response body means the token is expired or revoked,
480 # so we flag the credentials as such.
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400481 logger.error('Failed to retrieve access token: %s' % content)
Joe Gregorioccc79542011-02-19 00:05:26 -0500482 error_msg = 'Invalid response %s.' % resp['status']
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500483 try:
484 d = simplejson.loads(content)
485 if 'error' in d:
Joe Gregorioccc79542011-02-19 00:05:26 -0500486 error_msg = d['error']
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400487 self.invalid = True
488 if self.store:
489 self.store.locked_put(self)
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500490 except:
491 pass
Joe Gregorioccc79542011-02-19 00:05:26 -0500492 raise AccessTokenRefreshError(error_msg)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500493
494 def authorize(self, http):
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500495 """Authorize an httplib2.Http instance with these credentials.
496
Joe Gregorio695fdc12011-01-16 16:46:55 -0500497 Args:
498 http: An instance of httplib2.Http
499 or something that acts like it.
500
501 Returns:
502 A modified instance of http that was passed in.
503
504 Example:
505
506 h = httplib2.Http()
507 h = credentials.authorize(h)
508
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400509 You can't create a new OAuth subclass of httplib2.Authenication
510 because it never gets passed the absolute URI, which is needed for
511 signing. So instead we have to overload 'request' with a closure
512 that adds in the Authorization header and then calls the original
513 version of 'request()'.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500514 """
515 request_orig = http.request
516
517 # The closure that will replace 'httplib2.Http.request'.
518 def new_request(uri, method='GET', body=None, headers=None,
519 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
520 connection_type=None):
JacobMoshenko8e905102011-06-20 09:53:10 -0400521 if not self.access_token:
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400522 logger.info('Attempting refresh to obtain initial access_token')
JacobMoshenko8e905102011-06-20 09:53:10 -0400523 self._refresh(request_orig)
524
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400525 # Modify the request headers to add the appropriate
526 # Authorization header.
527 if headers is None:
Joe Gregorio695fdc12011-01-16 16:46:55 -0500528 headers = {}
Joe Gregorio49e94d82011-01-28 16:36:13 -0500529 headers['authorization'] = 'OAuth ' + self.access_token
JacobMoshenkocb6d8912011-07-08 13:35:15 -0400530
531 if self.user_agent is not None:
532 if 'user-agent' in headers:
533 headers['user-agent'] = self.user_agent + ' ' + headers['user-agent']
534 else:
535 headers['user-agent'] = self.user_agent
JacobMoshenko8e905102011-06-20 09:53:10 -0400536
Joe Gregorio695fdc12011-01-16 16:46:55 -0500537 resp, content = request_orig(uri, method, body, headers,
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500538 redirections, connection_type)
JacobMoshenko8e905102011-06-20 09:53:10 -0400539
Joe Gregoriofd19cd32011-01-20 11:37:29 -0500540 if resp.status == 401:
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400541 logger.info('Refreshing due to a 401')
Joe Gregorio695fdc12011-01-16 16:46:55 -0500542 self._refresh(request_orig)
Joe Gregorioccc79542011-02-19 00:05:26 -0500543 headers['authorization'] = 'OAuth ' + self.access_token
Joe Gregorio695fdc12011-01-16 16:46:55 -0500544 return request_orig(uri, method, body, headers,
545 redirections, connection_type)
546 else:
547 return (resp, content)
548
549 http.request = new_request
550 return http
551
552
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500553class AccessTokenCredentials(OAuth2Credentials):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400554 """Credentials object for OAuth 2.0.
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500555
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400556 Credentials can be applied to an httplib2.Http object using the
557 authorize() method, which then signs each request from that object
558 with the OAuth 2.0 access token. This set of credentials is for the
559 use case where you have acquired an OAuth 2.0 access_token from
560 another place such as a JavaScript client or another web
561 application, and wish to use it from Python. Because only the
562 access_token is present it can not be refreshed and will in time
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500563 expire.
564
Joe Gregorio9ce4b622011-02-17 15:32:11 -0500565 AccessTokenCredentials objects may be safely pickled and unpickled.
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500566
567 Usage:
568 credentials = AccessTokenCredentials('<an access token>',
569 'my-user-agent/1.0')
570 http = httplib2.Http()
571 http = credentials.authorize(http)
572
573 Exceptions:
574 AccessTokenCredentialsExpired: raised when the access_token expires or is
575 revoked.
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500576 """
577
578 def __init__(self, access_token, user_agent):
579 """Create an instance of OAuth2Credentials
580
581 This is one of the few types if Credentials that you should contrust,
582 Credentials objects are usually instantiated by a Flow.
583
584 Args:
ade@google.com93a7f7c2011-02-23 16:00:37 +0000585 access_token: string, access token.
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500586 user_agent: string, The HTTP User-Agent to provide for this application.
587
588 Notes:
589 store: callable, a callable that when passed a Credential
590 will store the credential back to where it came from.
591 """
592 super(AccessTokenCredentials, self).__init__(
593 access_token,
594 None,
595 None,
596 None,
597 None,
598 None,
599 user_agent)
600
Joe Gregorio562b7312011-09-15 09:06:38 -0400601
602 @classmethod
603 def from_json(cls, s):
604 data = simplejson.loads(s)
605 retval = AccessTokenCredentials(
606 data['access_token'],
607 data['user_agent'])
608 return retval
609
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500610 def _refresh(self, http_request):
611 raise AccessTokenCredentialsError(
612 "The access_token is expired or invalid and can't be refreshed.")
613
JacobMoshenko8e905102011-06-20 09:53:10 -0400614
615class AssertionCredentials(OAuth2Credentials):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400616 """Abstract Credentials object used for OAuth 2.0 assertion grants.
JacobMoshenko8e905102011-06-20 09:53:10 -0400617
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400618 This credential does not require a flow to instantiate because it
619 represents a two legged flow, and therefore has all of the required
620 information to generate and refresh its own access tokens. It must
621 be subclassed to generate the appropriate assertion string.
JacobMoshenko8e905102011-06-20 09:53:10 -0400622
623 AssertionCredentials objects may be safely pickled and unpickled.
624 """
625
626 def __init__(self, assertion_type, user_agent,
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400627 token_uri='https://accounts.google.com/o/oauth2/token',
628 **unused_kwargs):
629 """Constructor for AssertionFlowCredentials.
JacobMoshenko8e905102011-06-20 09:53:10 -0400630
631 Args:
632 assertion_type: string, assertion type that will be declared to the auth
633 server
634 user_agent: string, The HTTP User-Agent to provide for this application.
635 token_uri: string, URI for token endpoint. For convenience
636 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
637 """
638 super(AssertionCredentials, self).__init__(
639 None,
640 None,
641 None,
642 None,
643 None,
644 token_uri,
645 user_agent)
646 self.assertion_type = assertion_type
647
648 def _generate_refresh_request_body(self):
649 assertion = self._generate_assertion()
650
651 body = urllib.urlencode({
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400652 'assertion_type': self.assertion_type,
653 'assertion': assertion,
654 'grant_type': 'assertion',
655 })
JacobMoshenko8e905102011-06-20 09:53:10 -0400656
657 return body
658
659 def _generate_assertion(self):
660 """Generate the assertion string that will be used in the access token
661 request.
662 """
663 _abstract()
664
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500665if HAS_OPENSSL:
666 # PyOpenSSL is not a prerequisite for oauth2client, so if it is missing then
667 # don't create the SignedJwtAssertionCredentials or the verify_id_token()
668 # method.
669
670 class SignedJwtAssertionCredentials(AssertionCredentials):
671 """Credentials object used for OAuth 2.0 Signed JWT assertion grants.
672
673 This credential does not require a flow to instantiate because it
674 represents a two legged flow, and therefore has all of the required
675 information to generate and refresh its own access tokens.
676 """
677
678 MAX_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds
679
680 def __init__(self,
681 service_account_name,
682 private_key,
683 scope,
684 private_key_password='notasecret',
685 user_agent=None,
686 token_uri='https://accounts.google.com/o/oauth2/token',
687 **kwargs):
688 """Constructor for SignedJwtAssertionCredentials.
689
690 Args:
691 service_account_name: string, id for account, usually an email address.
692 private_key: string, private key in P12 format.
693 scope: string or list of strings, scope(s) of the credentials being
694 requested.
695 private_key_password: string, password for private_key.
696 user_agent: string, HTTP User-Agent to provide for this application.
697 token_uri: string, URI for token endpoint. For convenience
698 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
699 kwargs: kwargs, Additional parameters to add to the JWT token, for
700 example prn=joe@xample.org."""
701
702 super(SignedJwtAssertionCredentials, self).__init__(
703 'http://oauth.net/grant_type/jwt/1.0/bearer',
704 user_agent,
705 token_uri=token_uri,
706 )
707
708 if type(scope) is list:
709 scope = ' '.join(scope)
710 self.scope = scope
711
712 self.private_key = private_key
713 self.private_key_password = private_key_password
714 self.service_account_name = service_account_name
715 self.kwargs = kwargs
716
717 @classmethod
718 def from_json(cls, s):
719 data = simplejson.loads(s)
720 retval = SignedJwtAssertionCredentials(
721 data['service_account_name'],
722 data['private_key'],
723 data['private_key_password'],
724 data['scope'],
725 data['user_agent'],
726 data['token_uri'],
727 data['kwargs']
728 )
729 retval.invalid = data['invalid']
730 return retval
731
732 def _generate_assertion(self):
733 """Generate the assertion that will be used in the request."""
734 now = long(time.time())
735 payload = {
736 'aud': self.token_uri,
737 'scope': self.scope,
738 'iat': now,
739 'exp': now + SignedJwtAssertionCredentials.MAX_TOKEN_LIFETIME_SECS,
740 'iss': self.service_account_name
741 }
742 payload.update(self.kwargs)
743 logging.debug(str(payload))
744
745 return make_signed_jwt(
746 Signer.from_string(self.private_key, self.private_key_password),
747 payload)
748
Joe Gregorio9f2f38f2012-02-06 12:53:00 -0500749 # Only used in verify_id_token(), which is always calling to the same URI
750 # for the certs.
751 _cached_http = httplib2.Http(MemoryCache())
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500752
753 def verify_id_token(id_token, audience, http=None,
754 cert_uri=ID_TOKEN_VERIFICATON_CERTS):
755 """Verifies a signed JWT id_token.
756
757 Args:
758 id_token: string, A Signed JWT.
759 audience: string, The audience 'aud' that the token should be for.
760 http: httplib2.Http, instance to use to make the HTTP request. Callers
761 should supply an instance that has caching enabled.
762 cert_uri: string, URI of the certificates in JSON format to
763 verify the JWT against.
764
765 Returns:
766 The deserialized JSON in the JWT.
767
768 Raises:
769 oauth2client.crypt.AppIdentityError if the JWT fails to verify.
770 """
771 if http is None:
Joe Gregorio9f2f38f2012-02-06 12:53:00 -0500772 http = _cached_http
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500773
774 resp, content = http.request(cert_uri)
775
776 if resp.status == 200:
777 certs = simplejson.loads(content)
778 return verify_signed_jwt_with_certs(id_token, certs, audience)
779 else:
780 raise VerifyJwtTokenError('Status code: %d' % resp.status)
781
782
783def _urlsafe_b64decode(b64string):
Joe Gregoriobd512b52011-12-06 15:39:26 -0500784 # Guard against unicode strings, which base64 can't handle.
785 b64string = b64string.encode('ascii')
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500786 padded = b64string + '=' * (4 - len(b64string) % 4)
787 return base64.urlsafe_b64decode(padded)
788
789
790def _extract_id_token(id_token):
791 """Extract the JSON payload from a JWT.
792
793 Does the extraction w/o checking the signature.
794
795 Args:
796 id_token: string, OAuth 2.0 id_token.
797
798 Returns:
799 object, The deserialized JSON payload.
800 """
801 segments = id_token.split('.')
802
803 if (len(segments) != 3):
804 raise VerifyJwtTokenError(
805 'Wrong number of segments in token: %s' % id_token)
806
807 return simplejson.loads(_urlsafe_b64decode(segments[1]))
808
JacobMoshenko8e905102011-06-20 09:53:10 -0400809
Joe Gregorio695fdc12011-01-16 16:46:55 -0500810class OAuth2WebServerFlow(Flow):
811 """Does the Web Server Flow for OAuth 2.0.
812
813 OAuth2Credentials objects may be safely pickled and unpickled.
814 """
815
Joe Gregoriof08a4982011-10-07 13:11:16 -0400816 def __init__(self, client_id, client_secret, scope, user_agent=None,
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400817 auth_uri='https://accounts.google.com/o/oauth2/auth',
818 token_uri='https://accounts.google.com/o/oauth2/token',
819 **kwargs):
820 """Constructor for OAuth2WebServerFlow.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500821
822 Args:
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500823 client_id: string, client identifier.
824 client_secret: string client secret.
Joe Gregoriof2f8a5a2011-10-14 15:11:29 -0400825 scope: string or list of strings, scope(s) of the credentials being
826 requested.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500827 user_agent: string, HTTP User-Agent to provide for this application.
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500828 auth_uri: string, URI for authorization endpoint. For convenience
829 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
830 token_uri: string, URI for token endpoint. For convenience
831 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500832 **kwargs: dict, The keyword arguments are all optional and required
833 parameters for the OAuth calls.
834 """
835 self.client_id = client_id
836 self.client_secret = client_secret
Joe Gregoriof2f8a5a2011-10-14 15:11:29 -0400837 if type(scope) is list:
838 scope = ' '.join(scope)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500839 self.scope = scope
840 self.user_agent = user_agent
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500841 self.auth_uri = auth_uri
Joe Gregorio695fdc12011-01-16 16:46:55 -0500842 self.token_uri = token_uri
Joe Gregorio69a0aca2011-11-03 10:47:32 -0400843 self.params = {
844 'access_type': 'offline',
845 }
846 self.params.update(kwargs)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500847 self.redirect_uri = None
848
Joe Gregoriof2326c02012-02-09 12:18:44 -0500849 def step1_get_authorize_url(self, redirect_uri=OOB_CALLBACK_URN):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500850 """Returns a URI to redirect to the provider.
851
852 Args:
Joe Gregoriof2326c02012-02-09 12:18:44 -0500853 redirect_uri: string, Either the string 'urn:ietf:wg:oauth:2.0:oob' for
854 a non-web-based application, or a URI that handles the callback from
855 the authorization server.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500856
Joe Gregoriof2326c02012-02-09 12:18:44 -0500857 If redirect_uri is 'urn:ietf:wg:oauth:2.0:oob' then pass in the
Joe Gregorio695fdc12011-01-16 16:46:55 -0500858 generated verification code to step2_exchange,
859 otherwise pass in the query parameters received
860 at the callback uri to step2_exchange.
861 """
862
863 self.redirect_uri = redirect_uri
864 query = {
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400865 'response_type': 'code',
866 'client_id': self.client_id,
867 'redirect_uri': redirect_uri,
868 'scope': self.scope,
869 }
Joe Gregorio695fdc12011-01-16 16:46:55 -0500870 query.update(self.params)
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500871 parts = list(urlparse.urlparse(self.auth_uri))
Joe Gregorio695fdc12011-01-16 16:46:55 -0500872 query.update(dict(parse_qsl(parts[4]))) # 4 is the index of the query part
873 parts[4] = urllib.urlencode(query)
874 return urlparse.urlunparse(parts)
875
Joe Gregorioccc79542011-02-19 00:05:26 -0500876 def step2_exchange(self, code, http=None):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500877 """Exhanges a code for OAuth2Credentials.
878
879 Args:
880 code: string or dict, either the code as a string, or a dictionary
881 of the query parameters to the redirect_uri, which contains
882 the code.
Joe Gregorioccc79542011-02-19 00:05:26 -0500883 http: httplib2.Http, optional http instance to use to do the fetch
Joe Gregorio695fdc12011-01-16 16:46:55 -0500884 """
885
886 if not (isinstance(code, str) or isinstance(code, unicode)):
887 code = code['code']
888
889 body = urllib.urlencode({
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400890 'grant_type': 'authorization_code',
891 'client_id': self.client_id,
892 'client_secret': self.client_secret,
893 'code': code,
894 'redirect_uri': self.redirect_uri,
895 'scope': self.scope,
896 })
Joe Gregorio695fdc12011-01-16 16:46:55 -0500897 headers = {
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400898 'content-type': 'application/x-www-form-urlencoded',
Joe Gregorio695fdc12011-01-16 16:46:55 -0500899 }
JacobMoshenkocb6d8912011-07-08 13:35:15 -0400900
901 if self.user_agent is not None:
902 headers['user-agent'] = self.user_agent
903
Joe Gregorioccc79542011-02-19 00:05:26 -0500904 if http is None:
905 http = httplib2.Http()
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500906
JacobMoshenko8e905102011-06-20 09:53:10 -0400907 resp, content = http.request(self.token_uri, method='POST', body=body,
908 headers=headers)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500909 if resp.status == 200:
910 # TODO(jcgregorio) Raise an error if simplejson.loads fails?
911 d = simplejson.loads(content)
912 access_token = d['access_token']
913 refresh_token = d.get('refresh_token', None)
914 token_expiry = None
915 if 'expires_in' in d:
Joe Gregorio562b7312011-09-15 09:06:38 -0400916 token_expiry = datetime.datetime.utcnow() + datetime.timedelta(
JacobMoshenko8e905102011-06-20 09:53:10 -0400917 seconds=int(d['expires_in']))
Joe Gregorio695fdc12011-01-16 16:46:55 -0500918
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500919 if 'id_token' in d:
920 d['id_token'] = _extract_id_token(d['id_token'])
921
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400922 logger.info('Successfully retrieved access token: %s' % content)
JacobMoshenko8e905102011-06-20 09:53:10 -0400923 return OAuth2Credentials(access_token, self.client_id,
924 self.client_secret, refresh_token, token_expiry,
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500925 self.token_uri, self.user_agent,
926 id_token=d.get('id_token', None))
Joe Gregorio695fdc12011-01-16 16:46:55 -0500927 else:
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400928 logger.error('Failed to retrieve access token: %s' % content)
Joe Gregorioccc79542011-02-19 00:05:26 -0500929 error_msg = 'Invalid response %s.' % resp['status']
930 try:
931 d = simplejson.loads(content)
932 if 'error' in d:
933 error_msg = d['error']
934 except:
935 pass
936
937 raise FlowExchangeError(error_msg)
Joe Gregoriof08a4982011-10-07 13:11:16 -0400938
939def flow_from_clientsecrets(filename, scope, message=None):
940 """Create a Flow from a clientsecrets file.
941
942 Will create the right kind of Flow based on the contents of the clientsecrets
943 file or will raise InvalidClientSecretsError for unknown types of Flows.
944
945 Args:
946 filename: string, File name of client secrets.
Joe Gregoriof2f8a5a2011-10-14 15:11:29 -0400947 scope: string or list of strings, scope(s) to request.
Joe Gregoriof08a4982011-10-07 13:11:16 -0400948 message: string, A friendly string to display to the user if the
949 clientsecrets file is missing or invalid. If message is provided then
950 sys.exit will be called in the case of an error. If message in not
951 provided then clientsecrets.InvalidClientSecretsError will be raised.
952
953 Returns:
954 A Flow object.
955
956 Raises:
957 UnknownClientSecretsFlowError if the file describes an unknown kind of Flow.
958 clientsecrets.InvalidClientSecretsError if the clientsecrets file is
959 invalid.
960 """
Joe Gregorio0984ef22011-10-14 13:17:43 -0400961 try:
962 client_type, client_info = clientsecrets.loadfile(filename)
963 if client_type in [clientsecrets.TYPE_WEB, clientsecrets.TYPE_INSTALLED]:
964 return OAuth2WebServerFlow(
965 client_info['client_id'],
966 client_info['client_secret'],
967 scope,
968 None, # user_agent
969 client_info['auth_uri'],
970 client_info['token_uri'])
971 except clientsecrets.InvalidClientSecretsError:
972 if message:
973 sys.exit(message)
974 else:
975 raise
Joe Gregoriof08a4982011-10-07 13:11:16 -0400976 else:
977 raise UnknownClientSecretsFlowError(
978 'This OAuth 2.0 flow is unsupported: "%s"' * client_type)