blob: f492563a60e0f06748f24167860a5ba65409c798 [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 Gregorioec75dc12012-02-06 13:40:42 -0500227 def locked_delete(self):
228 """Delete a credential.
229
230 The Storage lock must be held when this is called.
231 """
232 _abstract()
233
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400234 def get(self):
235 """Retrieve credential.
236
237 The Storage lock must *not* be held when this is called.
238
239 Returns:
240 oauth2client.client.Credentials
241 """
242 self.acquire_lock()
243 try:
244 return self.locked_get()
245 finally:
246 self.release_lock()
247
248 def put(self, credentials):
249 """Write a credential.
250
251 The Storage lock must be held when this is called.
252
253 Args:
254 credentials: Credentials, the credentials to store.
255 """
256 self.acquire_lock()
257 try:
258 self.locked_put(credentials)
259 finally:
260 self.release_lock()
261
Joe Gregorioec75dc12012-02-06 13:40:42 -0500262 def delete(self):
263 """Delete credential.
264
265 Frees any resources associated with storing the credential.
266 The Storage lock must *not* be held when this is called.
267
268 Returns:
269 None
270 """
271 self.acquire_lock()
272 try:
273 return self.locked_delete()
274 finally:
275 self.release_lock()
276
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500277
Joe Gregorio695fdc12011-01-16 16:46:55 -0500278class OAuth2Credentials(Credentials):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400279 """Credentials object for OAuth 2.0.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500280
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500281 Credentials can be applied to an httplib2.Http object using the authorize()
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500282 method, which then adds the OAuth 2.0 access token to each request.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500283
284 OAuth2Credentials objects may be safely pickled and unpickled.
285 """
286
287 def __init__(self, access_token, client_id, client_secret, refresh_token,
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500288 token_expiry, token_uri, user_agent, id_token=None):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400289 """Create an instance of OAuth2Credentials.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500290
291 This constructor is not usually called by the user, instead
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500292 OAuth2Credentials objects are instantiated by the OAuth2WebServerFlow.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500293
294 Args:
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400295 access_token: string, access token.
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500296 client_id: string, client identifier.
297 client_secret: string, client secret.
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500298 refresh_token: string, refresh token.
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400299 token_expiry: datetime, when the access_token expires.
300 token_uri: string, URI of token endpoint.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500301 user_agent: string, The HTTP User-Agent to provide for this application.
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500302 id_token: object, The identity of the resource owner.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500303
Joe Gregorio695fdc12011-01-16 16:46:55 -0500304 Notes:
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500305 store: callable, A callable that when passed a Credential
Joe Gregorio695fdc12011-01-16 16:46:55 -0500306 will store the credential back to where it came from.
307 This is needed to store the latest access_token if it
308 has expired and been refreshed.
309 """
310 self.access_token = access_token
311 self.client_id = client_id
312 self.client_secret = client_secret
313 self.refresh_token = refresh_token
314 self.store = None
315 self.token_expiry = token_expiry
316 self.token_uri = token_uri
317 self.user_agent = user_agent
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500318 self.id_token = id_token
Joe Gregorio695fdc12011-01-16 16:46:55 -0500319
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500320 # True if the credentials have been revoked or expired and can't be
321 # refreshed.
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400322 self.invalid = False
Joe Gregorio9ce4b622011-02-17 15:32:11 -0500323
Joe Gregorio562b7312011-09-15 09:06:38 -0400324 def to_json(self):
325 return self._to_json(Credentials.NON_SERIALIZED_MEMBERS)
326
327 @classmethod
328 def from_json(cls, s):
329 """Instantiate a Credentials object from a JSON description of it. The JSON
330 should have been produced by calling .to_json() on the object.
331
332 Args:
333 data: dict, A deserialized JSON object.
334
335 Returns:
336 An instance of a Credentials subclass.
337 """
338 data = simplejson.loads(s)
339 if 'token_expiry' in data and not isinstance(data['token_expiry'],
340 datetime.datetime):
Joe Gregorio1daa71b2011-09-15 18:12:14 -0400341 try:
342 data['token_expiry'] = datetime.datetime.strptime(
343 data['token_expiry'], EXPIRY_FORMAT)
344 except:
345 data['token_expiry'] = None
Joe Gregorio562b7312011-09-15 09:06:38 -0400346 retval = OAuth2Credentials(
347 data['access_token'],
348 data['client_id'],
349 data['client_secret'],
350 data['refresh_token'],
351 data['token_expiry'],
352 data['token_uri'],
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500353 data['user_agent'],
354 data.get('id_token', None))
Joe Gregorio562b7312011-09-15 09:06:38 -0400355 retval.invalid = data['invalid']
356 return retval
357
Joe Gregorio9ce4b622011-02-17 15:32:11 -0500358 @property
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400359 def access_token_expired(self):
360 """True if the credential is expired or invalid.
361
362 If the token_expiry isn't set, we assume the token doesn't expire.
363 """
364 if self.invalid:
365 return True
366
367 if not self.token_expiry:
368 return False
369
Joe Gregorio562b7312011-09-15 09:06:38 -0400370 now = datetime.datetime.utcnow()
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400371 if now >= self.token_expiry:
372 logger.info('access_token is expired. Now: %s, token_expiry: %s',
373 now, self.token_expiry)
374 return True
375 return False
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500376
Joe Gregorio695fdc12011-01-16 16:46:55 -0500377 def set_store(self, store):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400378 """Set the Storage for the credential.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500379
380 Args:
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400381 store: Storage, an implementation of Stroage object.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500382 This is needed to store the latest access_token if it
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400383 has expired and been refreshed. This implementation uses
384 locking to check for updates before updating the
385 access_token.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500386 """
387 self.store = store
388
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400389 def _updateFromCredential(self, other):
390 """Update this Credential from another instance."""
391 self.__dict__.update(other.__getstate__())
392
Joe Gregorio695fdc12011-01-16 16:46:55 -0500393 def __getstate__(self):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400394 """Trim the state down to something that can be pickled."""
Joe Gregorio695fdc12011-01-16 16:46:55 -0500395 d = copy.copy(self.__dict__)
396 del d['store']
397 return d
398
399 def __setstate__(self, state):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400400 """Reconstitute the state of the object from being pickled."""
Joe Gregorio695fdc12011-01-16 16:46:55 -0500401 self.__dict__.update(state)
402 self.store = None
403
JacobMoshenko8e905102011-06-20 09:53:10 -0400404 def _generate_refresh_request_body(self):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400405 """Generate the body that will be used in the refresh request."""
JacobMoshenko8e905102011-06-20 09:53:10 -0400406 body = urllib.urlencode({
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400407 'grant_type': 'refresh_token',
408 'client_id': self.client_id,
409 'client_secret': self.client_secret,
410 'refresh_token': self.refresh_token,
411 })
JacobMoshenko8e905102011-06-20 09:53:10 -0400412 return body
413
414 def _generate_refresh_request_headers(self):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400415 """Generate the headers that will be used in the refresh request."""
JacobMoshenko8e905102011-06-20 09:53:10 -0400416 headers = {
JacobMoshenko8e905102011-06-20 09:53:10 -0400417 'content-type': 'application/x-www-form-urlencoded',
418 }
JacobMoshenkocb6d8912011-07-08 13:35:15 -0400419
420 if self.user_agent is not None:
421 headers['user-agent'] = self.user_agent
422
JacobMoshenko8e905102011-06-20 09:53:10 -0400423 return headers
424
Joe Gregorio695fdc12011-01-16 16:46:55 -0500425 def _refresh(self, http_request):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400426 """Refreshes the access_token.
427
428 This method first checks by reading the Storage object if available.
429 If a refresh is still needed, it holds the Storage lock until the
430 refresh is completed.
431 """
432 if not self.store:
433 self._do_refresh_request(http_request)
434 else:
435 self.store.acquire_lock()
436 try:
437 new_cred = self.store.locked_get()
438 if (new_cred and not new_cred.invalid and
439 new_cred.access_token != self.access_token):
440 logger.info('Updated access_token read from Storage')
441 self._updateFromCredential(new_cred)
442 else:
443 self._do_refresh_request(http_request)
444 finally:
445 self.store.release_lock()
446
447 def _do_refresh_request(self, http_request):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500448 """Refresh the access_token using the refresh_token.
449
450 Args:
451 http: An instance of httplib2.Http.request
452 or something that acts like it.
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400453
454 Raises:
455 AccessTokenRefreshError: When the refresh fails.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500456 """
JacobMoshenko8e905102011-06-20 09:53:10 -0400457 body = self._generate_refresh_request_body()
458 headers = self._generate_refresh_request_headers()
459
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400460 logger.info('Refresing access_token')
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500461 resp, content = http_request(
462 self.token_uri, method='POST', body=body, headers=headers)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500463 if resp.status == 200:
464 # TODO(jcgregorio) Raise an error if loads fails?
465 d = simplejson.loads(content)
466 self.access_token = d['access_token']
467 self.refresh_token = d.get('refresh_token', self.refresh_token)
468 if 'expires_in' in d:
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500469 self.token_expiry = datetime.timedelta(
Joe Gregorio562b7312011-09-15 09:06:38 -0400470 seconds=int(d['expires_in'])) + datetime.datetime.utcnow()
Joe Gregorio695fdc12011-01-16 16:46:55 -0500471 else:
472 self.token_expiry = None
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400473 if self.store:
474 self.store.locked_put(self)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500475 else:
JacobMoshenko8e905102011-06-20 09:53:10 -0400476 # An {'error':...} response body means the token is expired or revoked,
477 # so we flag the credentials as such.
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400478 logger.error('Failed to retrieve access token: %s' % content)
Joe Gregorioccc79542011-02-19 00:05:26 -0500479 error_msg = 'Invalid response %s.' % resp['status']
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500480 try:
481 d = simplejson.loads(content)
482 if 'error' in d:
Joe Gregorioccc79542011-02-19 00:05:26 -0500483 error_msg = d['error']
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400484 self.invalid = True
485 if self.store:
486 self.store.locked_put(self)
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500487 except:
488 pass
Joe Gregorioccc79542011-02-19 00:05:26 -0500489 raise AccessTokenRefreshError(error_msg)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500490
491 def authorize(self, http):
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500492 """Authorize an httplib2.Http instance with these credentials.
493
Joe Gregorio695fdc12011-01-16 16:46:55 -0500494 Args:
495 http: An instance of httplib2.Http
496 or something that acts like it.
497
498 Returns:
499 A modified instance of http that was passed in.
500
501 Example:
502
503 h = httplib2.Http()
504 h = credentials.authorize(h)
505
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400506 You can't create a new OAuth subclass of httplib2.Authenication
507 because it never gets passed the absolute URI, which is needed for
508 signing. So instead we have to overload 'request' with a closure
509 that adds in the Authorization header and then calls the original
510 version of 'request()'.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500511 """
512 request_orig = http.request
513
514 # The closure that will replace 'httplib2.Http.request'.
515 def new_request(uri, method='GET', body=None, headers=None,
516 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
517 connection_type=None):
JacobMoshenko8e905102011-06-20 09:53:10 -0400518 if not self.access_token:
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400519 logger.info('Attempting refresh to obtain initial access_token')
JacobMoshenko8e905102011-06-20 09:53:10 -0400520 self._refresh(request_orig)
521
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400522 # Modify the request headers to add the appropriate
523 # Authorization header.
524 if headers is None:
Joe Gregorio695fdc12011-01-16 16:46:55 -0500525 headers = {}
Joe Gregorio49e94d82011-01-28 16:36:13 -0500526 headers['authorization'] = 'OAuth ' + self.access_token
JacobMoshenkocb6d8912011-07-08 13:35:15 -0400527
528 if self.user_agent is not None:
529 if 'user-agent' in headers:
530 headers['user-agent'] = self.user_agent + ' ' + headers['user-agent']
531 else:
532 headers['user-agent'] = self.user_agent
JacobMoshenko8e905102011-06-20 09:53:10 -0400533
Joe Gregorio695fdc12011-01-16 16:46:55 -0500534 resp, content = request_orig(uri, method, body, headers,
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500535 redirections, connection_type)
JacobMoshenko8e905102011-06-20 09:53:10 -0400536
Joe Gregoriofd19cd32011-01-20 11:37:29 -0500537 if resp.status == 401:
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400538 logger.info('Refreshing due to a 401')
Joe Gregorio695fdc12011-01-16 16:46:55 -0500539 self._refresh(request_orig)
Joe Gregorioccc79542011-02-19 00:05:26 -0500540 headers['authorization'] = 'OAuth ' + self.access_token
Joe Gregorio695fdc12011-01-16 16:46:55 -0500541 return request_orig(uri, method, body, headers,
542 redirections, connection_type)
543 else:
544 return (resp, content)
545
546 http.request = new_request
547 return http
548
549
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500550class AccessTokenCredentials(OAuth2Credentials):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400551 """Credentials object for OAuth 2.0.
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500552
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400553 Credentials can be applied to an httplib2.Http object using the
554 authorize() method, which then signs each request from that object
555 with the OAuth 2.0 access token. This set of credentials is for the
556 use case where you have acquired an OAuth 2.0 access_token from
557 another place such as a JavaScript client or another web
558 application, and wish to use it from Python. Because only the
559 access_token is present it can not be refreshed and will in time
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500560 expire.
561
Joe Gregorio9ce4b622011-02-17 15:32:11 -0500562 AccessTokenCredentials objects may be safely pickled and unpickled.
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500563
564 Usage:
565 credentials = AccessTokenCredentials('<an access token>',
566 'my-user-agent/1.0')
567 http = httplib2.Http()
568 http = credentials.authorize(http)
569
570 Exceptions:
571 AccessTokenCredentialsExpired: raised when the access_token expires or is
572 revoked.
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500573 """
574
575 def __init__(self, access_token, user_agent):
576 """Create an instance of OAuth2Credentials
577
578 This is one of the few types if Credentials that you should contrust,
579 Credentials objects are usually instantiated by a Flow.
580
581 Args:
ade@google.com93a7f7c2011-02-23 16:00:37 +0000582 access_token: string, access token.
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500583 user_agent: string, The HTTP User-Agent to provide for this application.
584
585 Notes:
586 store: callable, a callable that when passed a Credential
587 will store the credential back to where it came from.
588 """
589 super(AccessTokenCredentials, self).__init__(
590 access_token,
591 None,
592 None,
593 None,
594 None,
595 None,
596 user_agent)
597
Joe Gregorio562b7312011-09-15 09:06:38 -0400598
599 @classmethod
600 def from_json(cls, s):
601 data = simplejson.loads(s)
602 retval = AccessTokenCredentials(
603 data['access_token'],
604 data['user_agent'])
605 return retval
606
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500607 def _refresh(self, http_request):
608 raise AccessTokenCredentialsError(
609 "The access_token is expired or invalid and can't be refreshed.")
610
JacobMoshenko8e905102011-06-20 09:53:10 -0400611
612class AssertionCredentials(OAuth2Credentials):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400613 """Abstract Credentials object used for OAuth 2.0 assertion grants.
JacobMoshenko8e905102011-06-20 09:53:10 -0400614
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400615 This credential does not require a flow to instantiate because it
616 represents a two legged flow, and therefore has all of the required
617 information to generate and refresh its own access tokens. It must
618 be subclassed to generate the appropriate assertion string.
JacobMoshenko8e905102011-06-20 09:53:10 -0400619
620 AssertionCredentials objects may be safely pickled and unpickled.
621 """
622
623 def __init__(self, assertion_type, user_agent,
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400624 token_uri='https://accounts.google.com/o/oauth2/token',
625 **unused_kwargs):
626 """Constructor for AssertionFlowCredentials.
JacobMoshenko8e905102011-06-20 09:53:10 -0400627
628 Args:
629 assertion_type: string, assertion type that will be declared to the auth
630 server
631 user_agent: string, The HTTP User-Agent to provide for this application.
632 token_uri: string, URI for token endpoint. For convenience
633 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
634 """
635 super(AssertionCredentials, self).__init__(
636 None,
637 None,
638 None,
639 None,
640 None,
641 token_uri,
642 user_agent)
643 self.assertion_type = assertion_type
644
645 def _generate_refresh_request_body(self):
646 assertion = self._generate_assertion()
647
648 body = urllib.urlencode({
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400649 'assertion_type': self.assertion_type,
650 'assertion': assertion,
651 'grant_type': 'assertion',
652 })
JacobMoshenko8e905102011-06-20 09:53:10 -0400653
654 return body
655
656 def _generate_assertion(self):
657 """Generate the assertion string that will be used in the access token
658 request.
659 """
660 _abstract()
661
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500662if HAS_OPENSSL:
663 # PyOpenSSL is not a prerequisite for oauth2client, so if it is missing then
664 # don't create the SignedJwtAssertionCredentials or the verify_id_token()
665 # method.
666
667 class SignedJwtAssertionCredentials(AssertionCredentials):
668 """Credentials object used for OAuth 2.0 Signed JWT assertion grants.
669
670 This credential does not require a flow to instantiate because it
671 represents a two legged flow, and therefore has all of the required
672 information to generate and refresh its own access tokens.
673 """
674
675 MAX_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds
676
677 def __init__(self,
678 service_account_name,
679 private_key,
680 scope,
681 private_key_password='notasecret',
682 user_agent=None,
683 token_uri='https://accounts.google.com/o/oauth2/token',
684 **kwargs):
685 """Constructor for SignedJwtAssertionCredentials.
686
687 Args:
688 service_account_name: string, id for account, usually an email address.
689 private_key: string, private key in P12 format.
690 scope: string or list of strings, scope(s) of the credentials being
691 requested.
692 private_key_password: string, password for private_key.
693 user_agent: string, HTTP User-Agent to provide for this application.
694 token_uri: string, URI for token endpoint. For convenience
695 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
696 kwargs: kwargs, Additional parameters to add to the JWT token, for
697 example prn=joe@xample.org."""
698
699 super(SignedJwtAssertionCredentials, self).__init__(
700 'http://oauth.net/grant_type/jwt/1.0/bearer',
701 user_agent,
702 token_uri=token_uri,
703 )
704
705 if type(scope) is list:
706 scope = ' '.join(scope)
707 self.scope = scope
708
709 self.private_key = private_key
710 self.private_key_password = private_key_password
711 self.service_account_name = service_account_name
712 self.kwargs = kwargs
713
714 @classmethod
715 def from_json(cls, s):
716 data = simplejson.loads(s)
717 retval = SignedJwtAssertionCredentials(
718 data['service_account_name'],
719 data['private_key'],
720 data['private_key_password'],
721 data['scope'],
722 data['user_agent'],
723 data['token_uri'],
724 data['kwargs']
725 )
726 retval.invalid = data['invalid']
727 return retval
728
729 def _generate_assertion(self):
730 """Generate the assertion that will be used in the request."""
731 now = long(time.time())
732 payload = {
733 'aud': self.token_uri,
734 'scope': self.scope,
735 'iat': now,
736 'exp': now + SignedJwtAssertionCredentials.MAX_TOKEN_LIFETIME_SECS,
737 'iss': self.service_account_name
738 }
739 payload.update(self.kwargs)
740 logging.debug(str(payload))
741
742 return make_signed_jwt(
743 Signer.from_string(self.private_key, self.private_key_password),
744 payload)
745
Joe Gregorio9f2f38f2012-02-06 12:53:00 -0500746 # Only used in verify_id_token(), which is always calling to the same URI
747 # for the certs.
748 _cached_http = httplib2.Http(MemoryCache())
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500749
750 def verify_id_token(id_token, audience, http=None,
751 cert_uri=ID_TOKEN_VERIFICATON_CERTS):
752 """Verifies a signed JWT id_token.
753
754 Args:
755 id_token: string, A Signed JWT.
756 audience: string, The audience 'aud' that the token should be for.
757 http: httplib2.Http, instance to use to make the HTTP request. Callers
758 should supply an instance that has caching enabled.
759 cert_uri: string, URI of the certificates in JSON format to
760 verify the JWT against.
761
762 Returns:
763 The deserialized JSON in the JWT.
764
765 Raises:
766 oauth2client.crypt.AppIdentityError if the JWT fails to verify.
767 """
768 if http is None:
Joe Gregorio9f2f38f2012-02-06 12:53:00 -0500769 http = _cached_http
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500770
771 resp, content = http.request(cert_uri)
772
773 if resp.status == 200:
774 certs = simplejson.loads(content)
775 return verify_signed_jwt_with_certs(id_token, certs, audience)
776 else:
777 raise VerifyJwtTokenError('Status code: %d' % resp.status)
778
779
780def _urlsafe_b64decode(b64string):
Joe Gregoriobd512b52011-12-06 15:39:26 -0500781 # Guard against unicode strings, which base64 can't handle.
782 b64string = b64string.encode('ascii')
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500783 padded = b64string + '=' * (4 - len(b64string) % 4)
784 return base64.urlsafe_b64decode(padded)
785
786
787def _extract_id_token(id_token):
788 """Extract the JSON payload from a JWT.
789
790 Does the extraction w/o checking the signature.
791
792 Args:
793 id_token: string, OAuth 2.0 id_token.
794
795 Returns:
796 object, The deserialized JSON payload.
797 """
798 segments = id_token.split('.')
799
800 if (len(segments) != 3):
801 raise VerifyJwtTokenError(
802 'Wrong number of segments in token: %s' % id_token)
803
804 return simplejson.loads(_urlsafe_b64decode(segments[1]))
805
JacobMoshenko8e905102011-06-20 09:53:10 -0400806
Joe Gregorio695fdc12011-01-16 16:46:55 -0500807class OAuth2WebServerFlow(Flow):
808 """Does the Web Server Flow for OAuth 2.0.
809
810 OAuth2Credentials objects may be safely pickled and unpickled.
811 """
812
Joe Gregoriof08a4982011-10-07 13:11:16 -0400813 def __init__(self, client_id, client_secret, scope, user_agent=None,
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400814 auth_uri='https://accounts.google.com/o/oauth2/auth',
815 token_uri='https://accounts.google.com/o/oauth2/token',
816 **kwargs):
817 """Constructor for OAuth2WebServerFlow.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500818
819 Args:
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500820 client_id: string, client identifier.
821 client_secret: string client secret.
Joe Gregoriof2f8a5a2011-10-14 15:11:29 -0400822 scope: string or list of strings, scope(s) of the credentials being
823 requested.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500824 user_agent: string, HTTP User-Agent to provide for this application.
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500825 auth_uri: string, URI for authorization endpoint. For convenience
826 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
827 token_uri: string, URI for token endpoint. For convenience
828 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500829 **kwargs: dict, The keyword arguments are all optional and required
830 parameters for the OAuth calls.
831 """
832 self.client_id = client_id
833 self.client_secret = client_secret
Joe Gregoriof2f8a5a2011-10-14 15:11:29 -0400834 if type(scope) is list:
835 scope = ' '.join(scope)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500836 self.scope = scope
837 self.user_agent = user_agent
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500838 self.auth_uri = auth_uri
Joe Gregorio695fdc12011-01-16 16:46:55 -0500839 self.token_uri = token_uri
Joe Gregorio69a0aca2011-11-03 10:47:32 -0400840 self.params = {
841 'access_type': 'offline',
842 }
843 self.params.update(kwargs)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500844 self.redirect_uri = None
845
846 def step1_get_authorize_url(self, redirect_uri='oob'):
847 """Returns a URI to redirect to the provider.
848
849 Args:
850 redirect_uri: string, Either the string 'oob' for a non-web-based
851 application, or a URI that handles the callback from
852 the authorization server.
853
854 If redirect_uri is 'oob' then pass in the
855 generated verification code to step2_exchange,
856 otherwise pass in the query parameters received
857 at the callback uri to step2_exchange.
858 """
859
860 self.redirect_uri = redirect_uri
861 query = {
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400862 'response_type': 'code',
863 'client_id': self.client_id,
864 'redirect_uri': redirect_uri,
865 'scope': self.scope,
866 }
Joe Gregorio695fdc12011-01-16 16:46:55 -0500867 query.update(self.params)
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500868 parts = list(urlparse.urlparse(self.auth_uri))
Joe Gregorio695fdc12011-01-16 16:46:55 -0500869 query.update(dict(parse_qsl(parts[4]))) # 4 is the index of the query part
870 parts[4] = urllib.urlencode(query)
871 return urlparse.urlunparse(parts)
872
Joe Gregorioccc79542011-02-19 00:05:26 -0500873 def step2_exchange(self, code, http=None):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500874 """Exhanges a code for OAuth2Credentials.
875
876 Args:
877 code: string or dict, either the code as a string, or a dictionary
878 of the query parameters to the redirect_uri, which contains
879 the code.
Joe Gregorioccc79542011-02-19 00:05:26 -0500880 http: httplib2.Http, optional http instance to use to do the fetch
Joe Gregorio695fdc12011-01-16 16:46:55 -0500881 """
882
883 if not (isinstance(code, str) or isinstance(code, unicode)):
884 code = code['code']
885
886 body = urllib.urlencode({
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400887 'grant_type': 'authorization_code',
888 'client_id': self.client_id,
889 'client_secret': self.client_secret,
890 'code': code,
891 'redirect_uri': self.redirect_uri,
892 'scope': self.scope,
893 })
Joe Gregorio695fdc12011-01-16 16:46:55 -0500894 headers = {
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400895 'content-type': 'application/x-www-form-urlencoded',
Joe Gregorio695fdc12011-01-16 16:46:55 -0500896 }
JacobMoshenkocb6d8912011-07-08 13:35:15 -0400897
898 if self.user_agent is not None:
899 headers['user-agent'] = self.user_agent
900
Joe Gregorioccc79542011-02-19 00:05:26 -0500901 if http is None:
902 http = httplib2.Http()
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500903
JacobMoshenko8e905102011-06-20 09:53:10 -0400904 resp, content = http.request(self.token_uri, method='POST', body=body,
905 headers=headers)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500906 if resp.status == 200:
907 # TODO(jcgregorio) Raise an error if simplejson.loads fails?
908 d = simplejson.loads(content)
909 access_token = d['access_token']
910 refresh_token = d.get('refresh_token', None)
911 token_expiry = None
912 if 'expires_in' in d:
Joe Gregorio562b7312011-09-15 09:06:38 -0400913 token_expiry = datetime.datetime.utcnow() + datetime.timedelta(
JacobMoshenko8e905102011-06-20 09:53:10 -0400914 seconds=int(d['expires_in']))
Joe Gregorio695fdc12011-01-16 16:46:55 -0500915
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500916 if 'id_token' in d:
917 d['id_token'] = _extract_id_token(d['id_token'])
918
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400919 logger.info('Successfully retrieved access token: %s' % content)
JacobMoshenko8e905102011-06-20 09:53:10 -0400920 return OAuth2Credentials(access_token, self.client_id,
921 self.client_secret, refresh_token, token_expiry,
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500922 self.token_uri, self.user_agent,
923 id_token=d.get('id_token', None))
Joe Gregorio695fdc12011-01-16 16:46:55 -0500924 else:
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400925 logger.error('Failed to retrieve access token: %s' % content)
Joe Gregorioccc79542011-02-19 00:05:26 -0500926 error_msg = 'Invalid response %s.' % resp['status']
927 try:
928 d = simplejson.loads(content)
929 if 'error' in d:
930 error_msg = d['error']
931 except:
932 pass
933
934 raise FlowExchangeError(error_msg)
Joe Gregoriof08a4982011-10-07 13:11:16 -0400935
936def flow_from_clientsecrets(filename, scope, message=None):
937 """Create a Flow from a clientsecrets file.
938
939 Will create the right kind of Flow based on the contents of the clientsecrets
940 file or will raise InvalidClientSecretsError for unknown types of Flows.
941
942 Args:
943 filename: string, File name of client secrets.
Joe Gregoriof2f8a5a2011-10-14 15:11:29 -0400944 scope: string or list of strings, scope(s) to request.
Joe Gregoriof08a4982011-10-07 13:11:16 -0400945 message: string, A friendly string to display to the user if the
946 clientsecrets file is missing or invalid. If message is provided then
947 sys.exit will be called in the case of an error. If message in not
948 provided then clientsecrets.InvalidClientSecretsError will be raised.
949
950 Returns:
951 A Flow object.
952
953 Raises:
954 UnknownClientSecretsFlowError if the file describes an unknown kind of Flow.
955 clientsecrets.InvalidClientSecretsError if the clientsecrets file is
956 invalid.
957 """
Joe Gregorio0984ef22011-10-14 13:17:43 -0400958 try:
959 client_type, client_info = clientsecrets.loadfile(filename)
960 if client_type in [clientsecrets.TYPE_WEB, clientsecrets.TYPE_INSTALLED]:
961 return OAuth2WebServerFlow(
962 client_info['client_id'],
963 client_info['client_secret'],
964 scope,
965 None, # user_agent
966 client_info['auth_uri'],
967 client_info['token_uri'])
968 except clientsecrets.InvalidClientSecretsError:
969 if message:
970 sys.exit(message)
971 else:
972 raise
Joe Gregoriof08a4982011-10-07 13:11:16 -0400973 else:
974 raise UnknownClientSecretsFlowError(
975 'This OAuth 2.0 flow is unsupported: "%s"' * client_type)