blob: 2d60f5a77d0c5568c0191161c188c0bc069b0a90 [file] [log] [blame]
Joe Gregorio20a5aa92011-04-01 17:44:25 -04001# Copyright (C) 2010 Google Inc.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
Joe Gregorio695fdc12011-01-16 16:46:55 -050014
Joe Gregorio9da2ad82011-09-11 14:04:44 -040015"""An OAuth 2.0 client.
Joe Gregorio695fdc12011-01-16 16:46:55 -050016
Joe Gregorio9da2ad82011-09-11 14:04:44 -040017Tools for interacting with OAuth 2.0 protected resources.
Joe Gregorio695fdc12011-01-16 16:46:55 -050018"""
19
20__author__ = 'jcgregorio@google.com (Joe Gregorio)'
21
Joe Gregorio8b4c1732011-12-06 11:28:29 -050022import base64
Joe Gregoriof08a4982011-10-07 13:11:16 -040023import clientsecrets
Joe Gregorio695fdc12011-01-16 16:46:55 -050024import copy
25import datetime
26import httplib2
27import logging
Joe Gregorio8b4c1732011-12-06 11:28:29 -050028import os
Joe Gregoriof08a4982011-10-07 13:11:16 -040029import sys
Joe Gregorio8b4c1732011-12-06 11:28:29 -050030import time
Joe Gregorio695fdc12011-01-16 16:46:55 -050031import urllib
32import urlparse
33
Joe Gregorio549230c2012-01-11 10:38:05 -050034from anyjson import simplejson
Joe Gregorio8b4c1732011-12-06 11:28:29 -050035
36HAS_OPENSSL = False
37try:
38 from oauth2client.crypt import Signer
39 from oauth2client.crypt import make_signed_jwt
40 from oauth2client.crypt import verify_signed_jwt_with_certs
41 HAS_OPENSSL = True
42except ImportError:
43 pass
44
Joe Gregorio695fdc12011-01-16 16:46:55 -050045try:
Joe Gregorio9da2ad82011-09-11 14:04:44 -040046 from urlparse import parse_qsl
Joe Gregorio695fdc12011-01-16 16:46:55 -050047except ImportError:
Joe Gregorio9da2ad82011-09-11 14:04:44 -040048 from cgi import parse_qsl
49
50logger = logging.getLogger(__name__)
Joe Gregorio695fdc12011-01-16 16:46:55 -050051
Joe Gregorio562b7312011-09-15 09:06:38 -040052# Expiry is stored in RFC3339 UTC format
Joe Gregorio8b4c1732011-12-06 11:28:29 -050053EXPIRY_FORMAT = '%Y-%m-%dT%H:%M:%SZ'
54
55# Which certs to use to validate id_tokens received.
56ID_TOKEN_VERIFICATON_CERTS = 'https://www.googleapis.com/oauth2/v1/certs'
Joe Gregorio562b7312011-09-15 09:06:38 -040057
Joe Gregorio695fdc12011-01-16 16:46:55 -050058
59class Error(Exception):
60 """Base error for this module."""
61 pass
62
63
Joe Gregorioccc79542011-02-19 00:05:26 -050064class FlowExchangeError(Error):
Joe Gregorioca876e42011-02-22 19:39:42 -050065 """Error trying to exchange an authorization grant for an access token."""
Joe Gregorioccc79542011-02-19 00:05:26 -050066 pass
67
68
69class AccessTokenRefreshError(Error):
Joe Gregorioca876e42011-02-22 19:39:42 -050070 """Error trying to refresh an expired access token."""
Joe Gregorio695fdc12011-01-16 16:46:55 -050071 pass
72
Joe Gregoriof08a4982011-10-07 13:11:16 -040073class UnknownClientSecretsFlowError(Error):
74 """The client secrets file called for an unknown type of OAuth 2.0 flow. """
75 pass
76
Joe Gregorio695fdc12011-01-16 16:46:55 -050077
Joe Gregorio3b79fa82011-02-17 11:47:17 -050078class AccessTokenCredentialsError(Error):
79 """Having only the access_token means no refresh is possible."""
Joe Gregorio695fdc12011-01-16 16:46:55 -050080 pass
81
82
Joe Gregorio8b4c1732011-12-06 11:28:29 -050083class VerifyJwtTokenError(Error):
84 """Could on retrieve certificates for validation."""
85 pass
86
87
Joe Gregorio695fdc12011-01-16 16:46:55 -050088def _abstract():
89 raise NotImplementedError('You need to override this function')
90
91
Joe Gregorio9f2f38f2012-02-06 12:53:00 -050092class MemoryCache(object):
93 """httplib2 Cache implementation which only caches locally."""
94
95 def __init__(self):
96 self.cache = {}
97
98 def get(self, key):
99 return self.cache.get(key)
100
101 def set(self, key, value):
102 self.cache[key] = value
103
104 def delete(self, key):
105 self.cache.pop(key, None)
106
107
Joe Gregorio695fdc12011-01-16 16:46:55 -0500108class Credentials(object):
109 """Base class for all Credentials objects.
110
Joe Gregorio562b7312011-09-15 09:06:38 -0400111 Subclasses must define an authorize() method that applies the credentials to
112 an HTTP transport.
113
114 Subclasses must also specify a classmethod named 'from_json' that takes a JSON
115 string as input and returns an instaniated Crentials object.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500116 """
117
Joe Gregorio562b7312011-09-15 09:06:38 -0400118 NON_SERIALIZED_MEMBERS = ['store']
119
Joe Gregorio695fdc12011-01-16 16:46:55 -0500120 def authorize(self, http):
121 """Take an httplib2.Http instance (or equivalent) and
122 authorizes it for the set of credentials, usually by
123 replacing http.request() with a method that adds in
124 the appropriate headers and then delegates to the original
125 Http.request() method.
126 """
127 _abstract()
128
Joe Gregorio562b7312011-09-15 09:06:38 -0400129 def _to_json(self, strip):
130 """Utility function for creating a JSON representation of an instance of Credentials.
131
132 Args:
133 strip: array, An array of names of members to not include in the JSON.
134
135 Returns:
136 string, a JSON representation of this instance, suitable to pass to
137 from_json().
138 """
139 t = type(self)
140 d = copy.copy(self.__dict__)
141 for member in strip:
142 del d[member]
143 if 'token_expiry' in d and isinstance(d['token_expiry'], datetime.datetime):
144 d['token_expiry'] = d['token_expiry'].strftime(EXPIRY_FORMAT)
145 # Add in information we will need later to reconsistitue this instance.
146 d['_class'] = t.__name__
147 d['_module'] = t.__module__
148 return simplejson.dumps(d)
149
150 def to_json(self):
151 """Creating a JSON representation of an instance of Credentials.
152
153 Returns:
154 string, a JSON representation of this instance, suitable to pass to
155 from_json().
156 """
157 return self._to_json(Credentials.NON_SERIALIZED_MEMBERS)
158
159 @classmethod
160 def new_from_json(cls, s):
161 """Utility class method to instantiate a Credentials subclass from a JSON
162 representation produced by to_json().
163
164 Args:
165 s: string, JSON from to_json().
166
167 Returns:
168 An instance of the subclass of Credentials that was serialized with
169 to_json().
170 """
171 data = simplejson.loads(s)
172 # Find and call the right classmethod from_json() to restore the object.
173 module = data['_module']
Joe Gregorioe9b40f12011-10-13 10:03:28 -0400174 m = __import__(module, fromlist=module.split('.')[:-1])
Joe Gregorio562b7312011-09-15 09:06:38 -0400175 kls = getattr(m, data['_class'])
176 from_json = getattr(kls, 'from_json')
177 return from_json(s)
178
JacobMoshenko8e905102011-06-20 09:53:10 -0400179
Joe Gregorio695fdc12011-01-16 16:46:55 -0500180class Flow(object):
181 """Base class for all Flow objects."""
182 pass
183
184
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500185class Storage(object):
186 """Base class for all Storage objects.
187
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400188 Store and retrieve a single credential. This class supports locking
189 such that multiple processes and threads can operate on a single
190 store.
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500191 """
192
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400193 def acquire_lock(self):
194 """Acquires any lock necessary to access this Storage.
195
196 This lock is not reentrant."""
197 pass
198
199 def release_lock(self):
200 """Release the Storage lock.
201
202 Trying to release a lock that isn't held will result in a
203 RuntimeError.
204 """
205 pass
206
207 def locked_get(self):
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500208 """Retrieve credential.
209
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400210 The Storage lock must be held when this is called.
211
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500212 Returns:
Joe Gregorio06d852b2011-03-25 15:03:10 -0400213 oauth2client.client.Credentials
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500214 """
215 _abstract()
216
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400217 def locked_put(self, credentials):
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500218 """Write a credential.
219
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400220 The Storage lock must be held when this is called.
221
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500222 Args:
223 credentials: Credentials, the credentials to store.
224 """
225 _abstract()
226
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400227 def get(self):
228 """Retrieve credential.
229
230 The Storage lock must *not* be held when this is called.
231
232 Returns:
233 oauth2client.client.Credentials
234 """
235 self.acquire_lock()
236 try:
237 return self.locked_get()
238 finally:
239 self.release_lock()
240
241 def put(self, credentials):
242 """Write a credential.
243
244 The Storage lock must be held when this is called.
245
246 Args:
247 credentials: Credentials, the credentials to store.
248 """
249 self.acquire_lock()
250 try:
251 self.locked_put(credentials)
252 finally:
253 self.release_lock()
254
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500255
Joe Gregorio695fdc12011-01-16 16:46:55 -0500256class OAuth2Credentials(Credentials):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400257 """Credentials object for OAuth 2.0.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500258
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500259 Credentials can be applied to an httplib2.Http object using the authorize()
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500260 method, which then adds the OAuth 2.0 access token to each request.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500261
262 OAuth2Credentials objects may be safely pickled and unpickled.
263 """
264
265 def __init__(self, access_token, client_id, client_secret, refresh_token,
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500266 token_expiry, token_uri, user_agent, id_token=None):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400267 """Create an instance of OAuth2Credentials.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500268
269 This constructor is not usually called by the user, instead
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500270 OAuth2Credentials objects are instantiated by the OAuth2WebServerFlow.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500271
272 Args:
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400273 access_token: string, access token.
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500274 client_id: string, client identifier.
275 client_secret: string, client secret.
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500276 refresh_token: string, refresh token.
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400277 token_expiry: datetime, when the access_token expires.
278 token_uri: string, URI of token endpoint.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500279 user_agent: string, The HTTP User-Agent to provide for this application.
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500280 id_token: object, The identity of the resource owner.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500281
Joe Gregorio695fdc12011-01-16 16:46:55 -0500282 Notes:
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500283 store: callable, A callable that when passed a Credential
Joe Gregorio695fdc12011-01-16 16:46:55 -0500284 will store the credential back to where it came from.
285 This is needed to store the latest access_token if it
286 has expired and been refreshed.
287 """
288 self.access_token = access_token
289 self.client_id = client_id
290 self.client_secret = client_secret
291 self.refresh_token = refresh_token
292 self.store = None
293 self.token_expiry = token_expiry
294 self.token_uri = token_uri
295 self.user_agent = user_agent
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500296 self.id_token = id_token
Joe Gregorio695fdc12011-01-16 16:46:55 -0500297
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500298 # True if the credentials have been revoked or expired and can't be
299 # refreshed.
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400300 self.invalid = False
Joe Gregorio9ce4b622011-02-17 15:32:11 -0500301
Joe Gregorio562b7312011-09-15 09:06:38 -0400302 def to_json(self):
303 return self._to_json(Credentials.NON_SERIALIZED_MEMBERS)
304
305 @classmethod
306 def from_json(cls, s):
307 """Instantiate a Credentials object from a JSON description of it. The JSON
308 should have been produced by calling .to_json() on the object.
309
310 Args:
311 data: dict, A deserialized JSON object.
312
313 Returns:
314 An instance of a Credentials subclass.
315 """
316 data = simplejson.loads(s)
317 if 'token_expiry' in data and not isinstance(data['token_expiry'],
318 datetime.datetime):
Joe Gregorio1daa71b2011-09-15 18:12:14 -0400319 try:
320 data['token_expiry'] = datetime.datetime.strptime(
321 data['token_expiry'], EXPIRY_FORMAT)
322 except:
323 data['token_expiry'] = None
Joe Gregorio562b7312011-09-15 09:06:38 -0400324 retval = OAuth2Credentials(
325 data['access_token'],
326 data['client_id'],
327 data['client_secret'],
328 data['refresh_token'],
329 data['token_expiry'],
330 data['token_uri'],
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500331 data['user_agent'],
332 data.get('id_token', None))
Joe Gregorio562b7312011-09-15 09:06:38 -0400333 retval.invalid = data['invalid']
334 return retval
335
Joe Gregorio9ce4b622011-02-17 15:32:11 -0500336 @property
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400337 def access_token_expired(self):
338 """True if the credential is expired or invalid.
339
340 If the token_expiry isn't set, we assume the token doesn't expire.
341 """
342 if self.invalid:
343 return True
344
345 if not self.token_expiry:
346 return False
347
Joe Gregorio562b7312011-09-15 09:06:38 -0400348 now = datetime.datetime.utcnow()
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400349 if now >= self.token_expiry:
350 logger.info('access_token is expired. Now: %s, token_expiry: %s',
351 now, self.token_expiry)
352 return True
353 return False
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500354
Joe Gregorio695fdc12011-01-16 16:46:55 -0500355 def set_store(self, store):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400356 """Set the Storage for the credential.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500357
358 Args:
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400359 store: Storage, an implementation of Stroage object.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500360 This is needed to store the latest access_token if it
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400361 has expired and been refreshed. This implementation uses
362 locking to check for updates before updating the
363 access_token.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500364 """
365 self.store = store
366
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400367 def _updateFromCredential(self, other):
368 """Update this Credential from another instance."""
369 self.__dict__.update(other.__getstate__())
370
Joe Gregorio695fdc12011-01-16 16:46:55 -0500371 def __getstate__(self):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400372 """Trim the state down to something that can be pickled."""
Joe Gregorio695fdc12011-01-16 16:46:55 -0500373 d = copy.copy(self.__dict__)
374 del d['store']
375 return d
376
377 def __setstate__(self, state):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400378 """Reconstitute the state of the object from being pickled."""
Joe Gregorio695fdc12011-01-16 16:46:55 -0500379 self.__dict__.update(state)
380 self.store = None
381
JacobMoshenko8e905102011-06-20 09:53:10 -0400382 def _generate_refresh_request_body(self):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400383 """Generate the body that will be used in the refresh request."""
JacobMoshenko8e905102011-06-20 09:53:10 -0400384 body = urllib.urlencode({
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400385 'grant_type': 'refresh_token',
386 'client_id': self.client_id,
387 'client_secret': self.client_secret,
388 'refresh_token': self.refresh_token,
389 })
JacobMoshenko8e905102011-06-20 09:53:10 -0400390 return body
391
392 def _generate_refresh_request_headers(self):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400393 """Generate the headers that will be used in the refresh request."""
JacobMoshenko8e905102011-06-20 09:53:10 -0400394 headers = {
JacobMoshenko8e905102011-06-20 09:53:10 -0400395 'content-type': 'application/x-www-form-urlencoded',
396 }
JacobMoshenkocb6d8912011-07-08 13:35:15 -0400397
398 if self.user_agent is not None:
399 headers['user-agent'] = self.user_agent
400
JacobMoshenko8e905102011-06-20 09:53:10 -0400401 return headers
402
Joe Gregorio695fdc12011-01-16 16:46:55 -0500403 def _refresh(self, http_request):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400404 """Refreshes the access_token.
405
406 This method first checks by reading the Storage object if available.
407 If a refresh is still needed, it holds the Storage lock until the
408 refresh is completed.
409 """
410 if not self.store:
411 self._do_refresh_request(http_request)
412 else:
413 self.store.acquire_lock()
414 try:
415 new_cred = self.store.locked_get()
416 if (new_cred and not new_cred.invalid and
417 new_cred.access_token != self.access_token):
418 logger.info('Updated access_token read from Storage')
419 self._updateFromCredential(new_cred)
420 else:
421 self._do_refresh_request(http_request)
422 finally:
423 self.store.release_lock()
424
425 def _do_refresh_request(self, http_request):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500426 """Refresh the access_token using the refresh_token.
427
428 Args:
429 http: An instance of httplib2.Http.request
430 or something that acts like it.
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400431
432 Raises:
433 AccessTokenRefreshError: When the refresh fails.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500434 """
JacobMoshenko8e905102011-06-20 09:53:10 -0400435 body = self._generate_refresh_request_body()
436 headers = self._generate_refresh_request_headers()
437
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400438 logger.info('Refresing access_token')
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500439 resp, content = http_request(
440 self.token_uri, method='POST', body=body, headers=headers)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500441 if resp.status == 200:
442 # TODO(jcgregorio) Raise an error if loads fails?
443 d = simplejson.loads(content)
444 self.access_token = d['access_token']
445 self.refresh_token = d.get('refresh_token', self.refresh_token)
446 if 'expires_in' in d:
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500447 self.token_expiry = datetime.timedelta(
Joe Gregorio562b7312011-09-15 09:06:38 -0400448 seconds=int(d['expires_in'])) + datetime.datetime.utcnow()
Joe Gregorio695fdc12011-01-16 16:46:55 -0500449 else:
450 self.token_expiry = None
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400451 if self.store:
452 self.store.locked_put(self)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500453 else:
JacobMoshenko8e905102011-06-20 09:53:10 -0400454 # An {'error':...} response body means the token is expired or revoked,
455 # so we flag the credentials as such.
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400456 logger.error('Failed to retrieve access token: %s' % content)
Joe Gregorioccc79542011-02-19 00:05:26 -0500457 error_msg = 'Invalid response %s.' % resp['status']
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500458 try:
459 d = simplejson.loads(content)
460 if 'error' in d:
Joe Gregorioccc79542011-02-19 00:05:26 -0500461 error_msg = d['error']
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400462 self.invalid = True
463 if self.store:
464 self.store.locked_put(self)
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500465 except:
466 pass
Joe Gregorioccc79542011-02-19 00:05:26 -0500467 raise AccessTokenRefreshError(error_msg)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500468
469 def authorize(self, http):
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500470 """Authorize an httplib2.Http instance with these credentials.
471
Joe Gregorio695fdc12011-01-16 16:46:55 -0500472 Args:
473 http: An instance of httplib2.Http
474 or something that acts like it.
475
476 Returns:
477 A modified instance of http that was passed in.
478
479 Example:
480
481 h = httplib2.Http()
482 h = credentials.authorize(h)
483
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400484 You can't create a new OAuth subclass of httplib2.Authenication
485 because it never gets passed the absolute URI, which is needed for
486 signing. So instead we have to overload 'request' with a closure
487 that adds in the Authorization header and then calls the original
488 version of 'request()'.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500489 """
490 request_orig = http.request
491
492 # The closure that will replace 'httplib2.Http.request'.
493 def new_request(uri, method='GET', body=None, headers=None,
494 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
495 connection_type=None):
JacobMoshenko8e905102011-06-20 09:53:10 -0400496 if not self.access_token:
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400497 logger.info('Attempting refresh to obtain initial access_token')
JacobMoshenko8e905102011-06-20 09:53:10 -0400498 self._refresh(request_orig)
499
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400500 # Modify the request headers to add the appropriate
501 # Authorization header.
502 if headers is None:
Joe Gregorio695fdc12011-01-16 16:46:55 -0500503 headers = {}
Joe Gregorio49e94d82011-01-28 16:36:13 -0500504 headers['authorization'] = 'OAuth ' + self.access_token
JacobMoshenkocb6d8912011-07-08 13:35:15 -0400505
506 if self.user_agent is not None:
507 if 'user-agent' in headers:
508 headers['user-agent'] = self.user_agent + ' ' + headers['user-agent']
509 else:
510 headers['user-agent'] = self.user_agent
JacobMoshenko8e905102011-06-20 09:53:10 -0400511
Joe Gregorio695fdc12011-01-16 16:46:55 -0500512 resp, content = request_orig(uri, method, body, headers,
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500513 redirections, connection_type)
JacobMoshenko8e905102011-06-20 09:53:10 -0400514
Joe Gregoriofd19cd32011-01-20 11:37:29 -0500515 if resp.status == 401:
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400516 logger.info('Refreshing due to a 401')
Joe Gregorio695fdc12011-01-16 16:46:55 -0500517 self._refresh(request_orig)
Joe Gregorioccc79542011-02-19 00:05:26 -0500518 headers['authorization'] = 'OAuth ' + self.access_token
Joe Gregorio695fdc12011-01-16 16:46:55 -0500519 return request_orig(uri, method, body, headers,
520 redirections, connection_type)
521 else:
522 return (resp, content)
523
524 http.request = new_request
525 return http
526
527
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500528class AccessTokenCredentials(OAuth2Credentials):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400529 """Credentials object for OAuth 2.0.
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500530
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400531 Credentials can be applied to an httplib2.Http object using the
532 authorize() method, which then signs each request from that object
533 with the OAuth 2.0 access token. This set of credentials is for the
534 use case where you have acquired an OAuth 2.0 access_token from
535 another place such as a JavaScript client or another web
536 application, and wish to use it from Python. Because only the
537 access_token is present it can not be refreshed and will in time
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500538 expire.
539
Joe Gregorio9ce4b622011-02-17 15:32:11 -0500540 AccessTokenCredentials objects may be safely pickled and unpickled.
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500541
542 Usage:
543 credentials = AccessTokenCredentials('<an access token>',
544 'my-user-agent/1.0')
545 http = httplib2.Http()
546 http = credentials.authorize(http)
547
548 Exceptions:
549 AccessTokenCredentialsExpired: raised when the access_token expires or is
550 revoked.
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500551 """
552
553 def __init__(self, access_token, user_agent):
554 """Create an instance of OAuth2Credentials
555
556 This is one of the few types if Credentials that you should contrust,
557 Credentials objects are usually instantiated by a Flow.
558
559 Args:
ade@google.com93a7f7c2011-02-23 16:00:37 +0000560 access_token: string, access token.
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500561 user_agent: string, The HTTP User-Agent to provide for this application.
562
563 Notes:
564 store: callable, a callable that when passed a Credential
565 will store the credential back to where it came from.
566 """
567 super(AccessTokenCredentials, self).__init__(
568 access_token,
569 None,
570 None,
571 None,
572 None,
573 None,
574 user_agent)
575
Joe Gregorio562b7312011-09-15 09:06:38 -0400576
577 @classmethod
578 def from_json(cls, s):
579 data = simplejson.loads(s)
580 retval = AccessTokenCredentials(
581 data['access_token'],
582 data['user_agent'])
583 return retval
584
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500585 def _refresh(self, http_request):
586 raise AccessTokenCredentialsError(
587 "The access_token is expired or invalid and can't be refreshed.")
588
JacobMoshenko8e905102011-06-20 09:53:10 -0400589
590class AssertionCredentials(OAuth2Credentials):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400591 """Abstract Credentials object used for OAuth 2.0 assertion grants.
JacobMoshenko8e905102011-06-20 09:53:10 -0400592
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400593 This credential does not require a flow to instantiate because it
594 represents a two legged flow, and therefore has all of the required
595 information to generate and refresh its own access tokens. It must
596 be subclassed to generate the appropriate assertion string.
JacobMoshenko8e905102011-06-20 09:53:10 -0400597
598 AssertionCredentials objects may be safely pickled and unpickled.
599 """
600
601 def __init__(self, assertion_type, user_agent,
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400602 token_uri='https://accounts.google.com/o/oauth2/token',
603 **unused_kwargs):
604 """Constructor for AssertionFlowCredentials.
JacobMoshenko8e905102011-06-20 09:53:10 -0400605
606 Args:
607 assertion_type: string, assertion type that will be declared to the auth
608 server
609 user_agent: string, The HTTP User-Agent to provide for this application.
610 token_uri: string, URI for token endpoint. For convenience
611 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
612 """
613 super(AssertionCredentials, self).__init__(
614 None,
615 None,
616 None,
617 None,
618 None,
619 token_uri,
620 user_agent)
621 self.assertion_type = assertion_type
622
623 def _generate_refresh_request_body(self):
624 assertion = self._generate_assertion()
625
626 body = urllib.urlencode({
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400627 'assertion_type': self.assertion_type,
628 'assertion': assertion,
629 'grant_type': 'assertion',
630 })
JacobMoshenko8e905102011-06-20 09:53:10 -0400631
632 return body
633
634 def _generate_assertion(self):
635 """Generate the assertion string that will be used in the access token
636 request.
637 """
638 _abstract()
639
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500640if HAS_OPENSSL:
641 # PyOpenSSL is not a prerequisite for oauth2client, so if it is missing then
642 # don't create the SignedJwtAssertionCredentials or the verify_id_token()
643 # method.
644
645 class SignedJwtAssertionCredentials(AssertionCredentials):
646 """Credentials object used for OAuth 2.0 Signed JWT assertion grants.
647
648 This credential does not require a flow to instantiate because it
649 represents a two legged flow, and therefore has all of the required
650 information to generate and refresh its own access tokens.
651 """
652
653 MAX_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds
654
655 def __init__(self,
656 service_account_name,
657 private_key,
658 scope,
659 private_key_password='notasecret',
660 user_agent=None,
661 token_uri='https://accounts.google.com/o/oauth2/token',
662 **kwargs):
663 """Constructor for SignedJwtAssertionCredentials.
664
665 Args:
666 service_account_name: string, id for account, usually an email address.
667 private_key: string, private key in P12 format.
668 scope: string or list of strings, scope(s) of the credentials being
669 requested.
670 private_key_password: string, password for private_key.
671 user_agent: string, HTTP User-Agent to provide for this application.
672 token_uri: string, URI for token endpoint. For convenience
673 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
674 kwargs: kwargs, Additional parameters to add to the JWT token, for
675 example prn=joe@xample.org."""
676
677 super(SignedJwtAssertionCredentials, self).__init__(
678 'http://oauth.net/grant_type/jwt/1.0/bearer',
679 user_agent,
680 token_uri=token_uri,
681 )
682
683 if type(scope) is list:
684 scope = ' '.join(scope)
685 self.scope = scope
686
687 self.private_key = private_key
688 self.private_key_password = private_key_password
689 self.service_account_name = service_account_name
690 self.kwargs = kwargs
691
692 @classmethod
693 def from_json(cls, s):
694 data = simplejson.loads(s)
695 retval = SignedJwtAssertionCredentials(
696 data['service_account_name'],
697 data['private_key'],
698 data['private_key_password'],
699 data['scope'],
700 data['user_agent'],
701 data['token_uri'],
702 data['kwargs']
703 )
704 retval.invalid = data['invalid']
705 return retval
706
707 def _generate_assertion(self):
708 """Generate the assertion that will be used in the request."""
709 now = long(time.time())
710 payload = {
711 'aud': self.token_uri,
712 'scope': self.scope,
713 'iat': now,
714 'exp': now + SignedJwtAssertionCredentials.MAX_TOKEN_LIFETIME_SECS,
715 'iss': self.service_account_name
716 }
717 payload.update(self.kwargs)
718 logging.debug(str(payload))
719
720 return make_signed_jwt(
721 Signer.from_string(self.private_key, self.private_key_password),
722 payload)
723
Joe Gregorio9f2f38f2012-02-06 12:53:00 -0500724 # Only used in verify_id_token(), which is always calling to the same URI
725 # for the certs.
726 _cached_http = httplib2.Http(MemoryCache())
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500727
728 def verify_id_token(id_token, audience, http=None,
729 cert_uri=ID_TOKEN_VERIFICATON_CERTS):
730 """Verifies a signed JWT id_token.
731
732 Args:
733 id_token: string, A Signed JWT.
734 audience: string, The audience 'aud' that the token should be for.
735 http: httplib2.Http, instance to use to make the HTTP request. Callers
736 should supply an instance that has caching enabled.
737 cert_uri: string, URI of the certificates in JSON format to
738 verify the JWT against.
739
740 Returns:
741 The deserialized JSON in the JWT.
742
743 Raises:
744 oauth2client.crypt.AppIdentityError if the JWT fails to verify.
745 """
746 if http is None:
Joe Gregorio9f2f38f2012-02-06 12:53:00 -0500747 http = _cached_http
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500748
749 resp, content = http.request(cert_uri)
750
751 if resp.status == 200:
752 certs = simplejson.loads(content)
753 return verify_signed_jwt_with_certs(id_token, certs, audience)
754 else:
755 raise VerifyJwtTokenError('Status code: %d' % resp.status)
756
757
758def _urlsafe_b64decode(b64string):
Joe Gregoriobd512b52011-12-06 15:39:26 -0500759 # Guard against unicode strings, which base64 can't handle.
760 b64string = b64string.encode('ascii')
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500761 padded = b64string + '=' * (4 - len(b64string) % 4)
762 return base64.urlsafe_b64decode(padded)
763
764
765def _extract_id_token(id_token):
766 """Extract the JSON payload from a JWT.
767
768 Does the extraction w/o checking the signature.
769
770 Args:
771 id_token: string, OAuth 2.0 id_token.
772
773 Returns:
774 object, The deserialized JSON payload.
775 """
776 segments = id_token.split('.')
777
778 if (len(segments) != 3):
779 raise VerifyJwtTokenError(
780 'Wrong number of segments in token: %s' % id_token)
781
782 return simplejson.loads(_urlsafe_b64decode(segments[1]))
783
JacobMoshenko8e905102011-06-20 09:53:10 -0400784
Joe Gregorio695fdc12011-01-16 16:46:55 -0500785class OAuth2WebServerFlow(Flow):
786 """Does the Web Server Flow for OAuth 2.0.
787
788 OAuth2Credentials objects may be safely pickled and unpickled.
789 """
790
Joe Gregoriof08a4982011-10-07 13:11:16 -0400791 def __init__(self, client_id, client_secret, scope, user_agent=None,
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400792 auth_uri='https://accounts.google.com/o/oauth2/auth',
793 token_uri='https://accounts.google.com/o/oauth2/token',
794 **kwargs):
795 """Constructor for OAuth2WebServerFlow.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500796
797 Args:
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500798 client_id: string, client identifier.
799 client_secret: string client secret.
Joe Gregoriof2f8a5a2011-10-14 15:11:29 -0400800 scope: string or list of strings, scope(s) of the credentials being
801 requested.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500802 user_agent: string, HTTP User-Agent to provide for this application.
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500803 auth_uri: string, URI for authorization endpoint. For convenience
804 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
805 token_uri: string, URI for token endpoint. For convenience
806 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500807 **kwargs: dict, The keyword arguments are all optional and required
808 parameters for the OAuth calls.
809 """
810 self.client_id = client_id
811 self.client_secret = client_secret
Joe Gregoriof2f8a5a2011-10-14 15:11:29 -0400812 if type(scope) is list:
813 scope = ' '.join(scope)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500814 self.scope = scope
815 self.user_agent = user_agent
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500816 self.auth_uri = auth_uri
Joe Gregorio695fdc12011-01-16 16:46:55 -0500817 self.token_uri = token_uri
Joe Gregorio69a0aca2011-11-03 10:47:32 -0400818 self.params = {
819 'access_type': 'offline',
820 }
821 self.params.update(kwargs)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500822 self.redirect_uri = None
823
824 def step1_get_authorize_url(self, redirect_uri='oob'):
825 """Returns a URI to redirect to the provider.
826
827 Args:
828 redirect_uri: string, Either the string 'oob' for a non-web-based
829 application, or a URI that handles the callback from
830 the authorization server.
831
832 If redirect_uri is 'oob' then pass in the
833 generated verification code to step2_exchange,
834 otherwise pass in the query parameters received
835 at the callback uri to step2_exchange.
836 """
837
838 self.redirect_uri = redirect_uri
839 query = {
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400840 'response_type': 'code',
841 'client_id': self.client_id,
842 'redirect_uri': redirect_uri,
843 'scope': self.scope,
844 }
Joe Gregorio695fdc12011-01-16 16:46:55 -0500845 query.update(self.params)
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500846 parts = list(urlparse.urlparse(self.auth_uri))
Joe Gregorio695fdc12011-01-16 16:46:55 -0500847 query.update(dict(parse_qsl(parts[4]))) # 4 is the index of the query part
848 parts[4] = urllib.urlencode(query)
849 return urlparse.urlunparse(parts)
850
Joe Gregorioccc79542011-02-19 00:05:26 -0500851 def step2_exchange(self, code, http=None):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500852 """Exhanges a code for OAuth2Credentials.
853
854 Args:
855 code: string or dict, either the code as a string, or a dictionary
856 of the query parameters to the redirect_uri, which contains
857 the code.
Joe Gregorioccc79542011-02-19 00:05:26 -0500858 http: httplib2.Http, optional http instance to use to do the fetch
Joe Gregorio695fdc12011-01-16 16:46:55 -0500859 """
860
861 if not (isinstance(code, str) or isinstance(code, unicode)):
862 code = code['code']
863
864 body = urllib.urlencode({
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400865 'grant_type': 'authorization_code',
866 'client_id': self.client_id,
867 'client_secret': self.client_secret,
868 'code': code,
869 'redirect_uri': self.redirect_uri,
870 'scope': self.scope,
871 })
Joe Gregorio695fdc12011-01-16 16:46:55 -0500872 headers = {
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400873 'content-type': 'application/x-www-form-urlencoded',
Joe Gregorio695fdc12011-01-16 16:46:55 -0500874 }
JacobMoshenkocb6d8912011-07-08 13:35:15 -0400875
876 if self.user_agent is not None:
877 headers['user-agent'] = self.user_agent
878
Joe Gregorioccc79542011-02-19 00:05:26 -0500879 if http is None:
880 http = httplib2.Http()
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500881
JacobMoshenko8e905102011-06-20 09:53:10 -0400882 resp, content = http.request(self.token_uri, method='POST', body=body,
883 headers=headers)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500884 if resp.status == 200:
885 # TODO(jcgregorio) Raise an error if simplejson.loads fails?
886 d = simplejson.loads(content)
887 access_token = d['access_token']
888 refresh_token = d.get('refresh_token', None)
889 token_expiry = None
890 if 'expires_in' in d:
Joe Gregorio562b7312011-09-15 09:06:38 -0400891 token_expiry = datetime.datetime.utcnow() + datetime.timedelta(
JacobMoshenko8e905102011-06-20 09:53:10 -0400892 seconds=int(d['expires_in']))
Joe Gregorio695fdc12011-01-16 16:46:55 -0500893
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500894 if 'id_token' in d:
895 d['id_token'] = _extract_id_token(d['id_token'])
896
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400897 logger.info('Successfully retrieved access token: %s' % content)
JacobMoshenko8e905102011-06-20 09:53:10 -0400898 return OAuth2Credentials(access_token, self.client_id,
899 self.client_secret, refresh_token, token_expiry,
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500900 self.token_uri, self.user_agent,
901 id_token=d.get('id_token', None))
Joe Gregorio695fdc12011-01-16 16:46:55 -0500902 else:
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400903 logger.error('Failed to retrieve access token: %s' % content)
Joe Gregorioccc79542011-02-19 00:05:26 -0500904 error_msg = 'Invalid response %s.' % resp['status']
905 try:
906 d = simplejson.loads(content)
907 if 'error' in d:
908 error_msg = d['error']
909 except:
910 pass
911
912 raise FlowExchangeError(error_msg)
Joe Gregoriof08a4982011-10-07 13:11:16 -0400913
914def flow_from_clientsecrets(filename, scope, message=None):
915 """Create a Flow from a clientsecrets file.
916
917 Will create the right kind of Flow based on the contents of the clientsecrets
918 file or will raise InvalidClientSecretsError for unknown types of Flows.
919
920 Args:
921 filename: string, File name of client secrets.
Joe Gregoriof2f8a5a2011-10-14 15:11:29 -0400922 scope: string or list of strings, scope(s) to request.
Joe Gregoriof08a4982011-10-07 13:11:16 -0400923 message: string, A friendly string to display to the user if the
924 clientsecrets file is missing or invalid. If message is provided then
925 sys.exit will be called in the case of an error. If message in not
926 provided then clientsecrets.InvalidClientSecretsError will be raised.
927
928 Returns:
929 A Flow object.
930
931 Raises:
932 UnknownClientSecretsFlowError if the file describes an unknown kind of Flow.
933 clientsecrets.InvalidClientSecretsError if the clientsecrets file is
934 invalid.
935 """
Joe Gregorio0984ef22011-10-14 13:17:43 -0400936 try:
937 client_type, client_info = clientsecrets.loadfile(filename)
938 if client_type in [clientsecrets.TYPE_WEB, clientsecrets.TYPE_INSTALLED]:
939 return OAuth2WebServerFlow(
940 client_info['client_id'],
941 client_info['client_secret'],
942 scope,
943 None, # user_agent
944 client_info['auth_uri'],
945 client_info['token_uri'])
946 except clientsecrets.InvalidClientSecretsError:
947 if message:
948 sys.exit(message)
949 else:
950 raise
Joe Gregoriof08a4982011-10-07 13:11:16 -0400951 else:
952 raise UnknownClientSecretsFlowError(
953 'This OAuth 2.0 flow is unsupported: "%s"' * client_type)