blob: f6f4a1ab040b66e1257af52bd7a8f54dc36cb73b [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
Joe Gregoriofa8cd9f2012-02-23 14:00:40 -0500118 string as input and returns an instaniated Credentials 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 Gregorio654f4a22012-02-09 14:15:44 -0500132 def refresh(self, http):
133 """Forces a refresh of the access_token.
134
135 Args:
136 http: httplib2.Http, an http object to be used to make the refresh
137 request.
138 """
139 _abstract()
140
141 def apply(self, headers):
142 """Add the authorization to the headers.
143
144 Args:
145 headers: dict, the headers to add the Authorization header to.
146 """
147 _abstract()
148
Joe Gregorio562b7312011-09-15 09:06:38 -0400149 def _to_json(self, strip):
150 """Utility function for creating a JSON representation of an instance of Credentials.
151
152 Args:
153 strip: array, An array of names of members to not include in the JSON.
154
155 Returns:
156 string, a JSON representation of this instance, suitable to pass to
157 from_json().
158 """
159 t = type(self)
160 d = copy.copy(self.__dict__)
161 for member in strip:
Joe Gregorio08cdcb82012-03-14 00:09:33 -0400162 if member in d:
163 del d[member]
Joe Gregorio562b7312011-09-15 09:06:38 -0400164 if 'token_expiry' in d and isinstance(d['token_expiry'], datetime.datetime):
165 d['token_expiry'] = d['token_expiry'].strftime(EXPIRY_FORMAT)
166 # Add in information we will need later to reconsistitue this instance.
167 d['_class'] = t.__name__
168 d['_module'] = t.__module__
169 return simplejson.dumps(d)
170
171 def to_json(self):
172 """Creating a JSON representation of an instance of Credentials.
173
174 Returns:
175 string, a JSON representation of this instance, suitable to pass to
176 from_json().
177 """
178 return self._to_json(Credentials.NON_SERIALIZED_MEMBERS)
179
180 @classmethod
181 def new_from_json(cls, s):
182 """Utility class method to instantiate a Credentials subclass from a JSON
183 representation produced by to_json().
184
185 Args:
186 s: string, JSON from to_json().
187
188 Returns:
189 An instance of the subclass of Credentials that was serialized with
190 to_json().
191 """
192 data = simplejson.loads(s)
193 # Find and call the right classmethod from_json() to restore the object.
194 module = data['_module']
Joe Gregoriofa8cd9f2012-02-23 14:00:40 -0500195 try:
196 m = __import__(module)
197 except ImportError:
198 # In case there's an object from the old package structure, update it
199 module = module.replace('.apiclient', '')
200 m = __import__(module)
201
Joe Gregorioe9b40f12011-10-13 10:03:28 -0400202 m = __import__(module, fromlist=module.split('.')[:-1])
Joe Gregorio562b7312011-09-15 09:06:38 -0400203 kls = getattr(m, data['_class'])
204 from_json = getattr(kls, 'from_json')
205 return from_json(s)
206
Joe Gregorio08cdcb82012-03-14 00:09:33 -0400207 @classmethod
208 def from_json(cls, s):
209 """Instantiate a Credentials object from a JSON description of it. The JSON
210 should have been produced by calling .to_json() on the object.
211
212 Args:
213 data: dict, A deserialized JSON object.
214
215 Returns:
216 An instance of a Credentials subclass.
217 """
218 return Credentials()
219
JacobMoshenko8e905102011-06-20 09:53:10 -0400220
Joe Gregorio695fdc12011-01-16 16:46:55 -0500221class Flow(object):
222 """Base class for all Flow objects."""
223 pass
224
225
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500226class Storage(object):
227 """Base class for all Storage objects.
228
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400229 Store and retrieve a single credential. This class supports locking
230 such that multiple processes and threads can operate on a single
231 store.
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500232 """
233
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400234 def acquire_lock(self):
235 """Acquires any lock necessary to access this Storage.
236
237 This lock is not reentrant."""
238 pass
239
240 def release_lock(self):
241 """Release the Storage lock.
242
243 Trying to release a lock that isn't held will result in a
244 RuntimeError.
245 """
246 pass
247
248 def locked_get(self):
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500249 """Retrieve credential.
250
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400251 The Storage lock must be held when this is called.
252
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500253 Returns:
Joe Gregorio06d852b2011-03-25 15:03:10 -0400254 oauth2client.client.Credentials
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500255 """
256 _abstract()
257
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400258 def locked_put(self, credentials):
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500259 """Write a credential.
260
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400261 The Storage lock must be held when this is called.
262
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500263 Args:
264 credentials: Credentials, the credentials to store.
265 """
266 _abstract()
267
Joe Gregorioec75dc12012-02-06 13:40:42 -0500268 def locked_delete(self):
269 """Delete a credential.
270
271 The Storage lock must be held when this is called.
272 """
273 _abstract()
274
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400275 def get(self):
276 """Retrieve credential.
277
278 The Storage lock must *not* be held when this is called.
279
280 Returns:
281 oauth2client.client.Credentials
282 """
283 self.acquire_lock()
284 try:
285 return self.locked_get()
286 finally:
287 self.release_lock()
288
289 def put(self, credentials):
290 """Write a credential.
291
292 The Storage lock must be held when this is called.
293
294 Args:
295 credentials: Credentials, the credentials to store.
296 """
297 self.acquire_lock()
298 try:
299 self.locked_put(credentials)
300 finally:
301 self.release_lock()
302
Joe Gregorioec75dc12012-02-06 13:40:42 -0500303 def delete(self):
304 """Delete credential.
305
306 Frees any resources associated with storing the credential.
307 The Storage lock must *not* be held when this is called.
308
309 Returns:
310 None
311 """
312 self.acquire_lock()
313 try:
314 return self.locked_delete()
315 finally:
316 self.release_lock()
317
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500318
Joe Gregorio695fdc12011-01-16 16:46:55 -0500319class OAuth2Credentials(Credentials):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400320 """Credentials object for OAuth 2.0.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500321
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500322 Credentials can be applied to an httplib2.Http object using the authorize()
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500323 method, which then adds the OAuth 2.0 access token to each request.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500324
325 OAuth2Credentials objects may be safely pickled and unpickled.
326 """
327
328 def __init__(self, access_token, client_id, client_secret, refresh_token,
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500329 token_expiry, token_uri, user_agent, id_token=None):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400330 """Create an instance of OAuth2Credentials.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500331
332 This constructor is not usually called by the user, instead
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500333 OAuth2Credentials objects are instantiated by the OAuth2WebServerFlow.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500334
335 Args:
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400336 access_token: string, access token.
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500337 client_id: string, client identifier.
338 client_secret: string, client secret.
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500339 refresh_token: string, refresh token.
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400340 token_expiry: datetime, when the access_token expires.
341 token_uri: string, URI of token endpoint.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500342 user_agent: string, The HTTP User-Agent to provide for this application.
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500343 id_token: object, The identity of the resource owner.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500344
Joe Gregorio695fdc12011-01-16 16:46:55 -0500345 Notes:
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500346 store: callable, A callable that when passed a Credential
Joe Gregorio695fdc12011-01-16 16:46:55 -0500347 will store the credential back to where it came from.
348 This is needed to store the latest access_token if it
349 has expired and been refreshed.
350 """
351 self.access_token = access_token
352 self.client_id = client_id
353 self.client_secret = client_secret
354 self.refresh_token = refresh_token
355 self.store = None
356 self.token_expiry = token_expiry
357 self.token_uri = token_uri
358 self.user_agent = user_agent
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500359 self.id_token = id_token
Joe Gregorio695fdc12011-01-16 16:46:55 -0500360
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500361 # True if the credentials have been revoked or expired and can't be
362 # refreshed.
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400363 self.invalid = False
Joe Gregorio9ce4b622011-02-17 15:32:11 -0500364
Joe Gregorio654f4a22012-02-09 14:15:44 -0500365 def authorize(self, http):
366 """Authorize an httplib2.Http instance with these credentials.
367
368 The modified http.request method will add authentication headers to each
369 request and will refresh access_tokens when a 401 is received on a
370 request. In addition the http.request method has a credentials property,
371 http.request.credentials, which is the Credentials object that authorized
372 it.
373
374 Args:
375 http: An instance of httplib2.Http
376 or something that acts like it.
377
378 Returns:
379 A modified instance of http that was passed in.
380
381 Example:
382
383 h = httplib2.Http()
384 h = credentials.authorize(h)
385
386 You can't create a new OAuth subclass of httplib2.Authenication
387 because it never gets passed the absolute URI, which is needed for
388 signing. So instead we have to overload 'request' with a closure
389 that adds in the Authorization header and then calls the original
390 version of 'request()'.
391 """
392 request_orig = http.request
393
394 # The closure that will replace 'httplib2.Http.request'.
395 def new_request(uri, method='GET', body=None, headers=None,
396 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
397 connection_type=None):
398 if not self.access_token:
399 logger.info('Attempting refresh to obtain initial access_token')
400 self._refresh(request_orig)
401
402 # Modify the request headers to add the appropriate
403 # Authorization header.
404 if headers is None:
405 headers = {}
406 self.apply(headers)
407
408 if self.user_agent is not None:
409 if 'user-agent' in headers:
410 headers['user-agent'] = self.user_agent + ' ' + headers['user-agent']
411 else:
412 headers['user-agent'] = self.user_agent
413
414 resp, content = request_orig(uri, method, body, headers,
415 redirections, connection_type)
416
417 if resp.status == 401:
418 logger.info('Refreshing due to a 401')
419 self._refresh(request_orig)
420 self.apply(headers)
421 return request_orig(uri, method, body, headers,
422 redirections, connection_type)
423 else:
424 return (resp, content)
425
426 # Replace the request method with our own closure.
427 http.request = new_request
428
429 # Set credentials as a property of the request method.
430 setattr(http.request, 'credentials', self)
431
432 return http
433
434 def refresh(self, http):
435 """Forces a refresh of the access_token.
436
437 Args:
438 http: httplib2.Http, an http object to be used to make the refresh
439 request.
440 """
441 self._refresh(http.request)
442
443 def apply(self, headers):
444 """Add the authorization to the headers.
445
446 Args:
447 headers: dict, the headers to add the Authorization header to.
448 """
449 headers['Authorization'] = 'Bearer ' + self.access_token
450
Joe Gregorio562b7312011-09-15 09:06:38 -0400451 def to_json(self):
452 return self._to_json(Credentials.NON_SERIALIZED_MEMBERS)
453
454 @classmethod
455 def from_json(cls, s):
456 """Instantiate a Credentials object from a JSON description of it. The JSON
457 should have been produced by calling .to_json() on the object.
458
459 Args:
460 data: dict, A deserialized JSON object.
461
462 Returns:
463 An instance of a Credentials subclass.
464 """
465 data = simplejson.loads(s)
466 if 'token_expiry' in data and not isinstance(data['token_expiry'],
467 datetime.datetime):
Joe Gregorio1daa71b2011-09-15 18:12:14 -0400468 try:
469 data['token_expiry'] = datetime.datetime.strptime(
470 data['token_expiry'], EXPIRY_FORMAT)
471 except:
472 data['token_expiry'] = None
Joe Gregorio562b7312011-09-15 09:06:38 -0400473 retval = OAuth2Credentials(
474 data['access_token'],
475 data['client_id'],
476 data['client_secret'],
477 data['refresh_token'],
478 data['token_expiry'],
479 data['token_uri'],
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500480 data['user_agent'],
481 data.get('id_token', None))
Joe Gregorio562b7312011-09-15 09:06:38 -0400482 retval.invalid = data['invalid']
483 return retval
484
Joe Gregorio9ce4b622011-02-17 15:32:11 -0500485 @property
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400486 def access_token_expired(self):
487 """True if the credential is expired or invalid.
488
489 If the token_expiry isn't set, we assume the token doesn't expire.
490 """
491 if self.invalid:
492 return True
493
494 if not self.token_expiry:
495 return False
496
Joe Gregorio562b7312011-09-15 09:06:38 -0400497 now = datetime.datetime.utcnow()
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400498 if now >= self.token_expiry:
499 logger.info('access_token is expired. Now: %s, token_expiry: %s',
500 now, self.token_expiry)
501 return True
502 return False
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500503
Joe Gregorio695fdc12011-01-16 16:46:55 -0500504 def set_store(self, store):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400505 """Set the Storage for the credential.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500506
507 Args:
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400508 store: Storage, an implementation of Stroage object.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500509 This is needed to store the latest access_token if it
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400510 has expired and been refreshed. This implementation uses
511 locking to check for updates before updating the
512 access_token.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500513 """
514 self.store = store
515
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400516 def _updateFromCredential(self, other):
517 """Update this Credential from another instance."""
518 self.__dict__.update(other.__getstate__())
519
Joe Gregorio695fdc12011-01-16 16:46:55 -0500520 def __getstate__(self):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400521 """Trim the state down to something that can be pickled."""
Joe Gregorio695fdc12011-01-16 16:46:55 -0500522 d = copy.copy(self.__dict__)
523 del d['store']
524 return d
525
526 def __setstate__(self, state):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400527 """Reconstitute the state of the object from being pickled."""
Joe Gregorio695fdc12011-01-16 16:46:55 -0500528 self.__dict__.update(state)
529 self.store = None
530
JacobMoshenko8e905102011-06-20 09:53:10 -0400531 def _generate_refresh_request_body(self):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400532 """Generate the body that will be used in the refresh request."""
JacobMoshenko8e905102011-06-20 09:53:10 -0400533 body = urllib.urlencode({
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400534 'grant_type': 'refresh_token',
535 'client_id': self.client_id,
536 'client_secret': self.client_secret,
537 'refresh_token': self.refresh_token,
538 })
JacobMoshenko8e905102011-06-20 09:53:10 -0400539 return body
540
541 def _generate_refresh_request_headers(self):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400542 """Generate the headers that will be used in the refresh request."""
JacobMoshenko8e905102011-06-20 09:53:10 -0400543 headers = {
JacobMoshenko8e905102011-06-20 09:53:10 -0400544 'content-type': 'application/x-www-form-urlencoded',
545 }
JacobMoshenkocb6d8912011-07-08 13:35:15 -0400546
547 if self.user_agent is not None:
548 headers['user-agent'] = self.user_agent
549
JacobMoshenko8e905102011-06-20 09:53:10 -0400550 return headers
551
Joe Gregorio695fdc12011-01-16 16:46:55 -0500552 def _refresh(self, http_request):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400553 """Refreshes the access_token.
554
555 This method first checks by reading the Storage object if available.
556 If a refresh is still needed, it holds the Storage lock until the
557 refresh is completed.
Joe Gregorio654f4a22012-02-09 14:15:44 -0500558
559 Args:
560 http_request: callable, a callable that matches the method signature of
561 httplib2.Http.request, used to make the refresh request.
562
563 Raises:
564 AccessTokenRefreshError: When the refresh fails.
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400565 """
566 if not self.store:
567 self._do_refresh_request(http_request)
568 else:
569 self.store.acquire_lock()
570 try:
571 new_cred = self.store.locked_get()
572 if (new_cred and not new_cred.invalid and
573 new_cred.access_token != self.access_token):
574 logger.info('Updated access_token read from Storage')
575 self._updateFromCredential(new_cred)
576 else:
577 self._do_refresh_request(http_request)
578 finally:
579 self.store.release_lock()
580
581 def _do_refresh_request(self, http_request):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500582 """Refresh the access_token using the refresh_token.
583
584 Args:
Joe Gregorio654f4a22012-02-09 14:15:44 -0500585 http_request: callable, a callable that matches the method signature of
586 httplib2.Http.request, used to make the refresh request.
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400587
588 Raises:
589 AccessTokenRefreshError: When the refresh fails.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500590 """
JacobMoshenko8e905102011-06-20 09:53:10 -0400591 body = self._generate_refresh_request_body()
592 headers = self._generate_refresh_request_headers()
593
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400594 logger.info('Refresing access_token')
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500595 resp, content = http_request(
596 self.token_uri, method='POST', body=body, headers=headers)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500597 if resp.status == 200:
598 # TODO(jcgregorio) Raise an error if loads fails?
599 d = simplejson.loads(content)
600 self.access_token = d['access_token']
601 self.refresh_token = d.get('refresh_token', self.refresh_token)
602 if 'expires_in' in d:
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500603 self.token_expiry = datetime.timedelta(
Joe Gregorio562b7312011-09-15 09:06:38 -0400604 seconds=int(d['expires_in'])) + datetime.datetime.utcnow()
Joe Gregorio695fdc12011-01-16 16:46:55 -0500605 else:
606 self.token_expiry = None
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400607 if self.store:
608 self.store.locked_put(self)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500609 else:
JacobMoshenko8e905102011-06-20 09:53:10 -0400610 # An {'error':...} response body means the token is expired or revoked,
611 # so we flag the credentials as such.
Joe Gregorioe78621a2012-03-09 15:47:23 -0500612 logger.info('Failed to retrieve access token: %s' % content)
Joe Gregorioccc79542011-02-19 00:05:26 -0500613 error_msg = 'Invalid response %s.' % resp['status']
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500614 try:
615 d = simplejson.loads(content)
616 if 'error' in d:
Joe Gregorioccc79542011-02-19 00:05:26 -0500617 error_msg = d['error']
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400618 self.invalid = True
619 if self.store:
620 self.store.locked_put(self)
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500621 except:
622 pass
Joe Gregorioccc79542011-02-19 00:05:26 -0500623 raise AccessTokenRefreshError(error_msg)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500624
Joe Gregorio695fdc12011-01-16 16:46:55 -0500625
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500626class AccessTokenCredentials(OAuth2Credentials):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400627 """Credentials object for OAuth 2.0.
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500628
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400629 Credentials can be applied to an httplib2.Http object using the
630 authorize() method, which then signs each request from that object
631 with the OAuth 2.0 access token. This set of credentials is for the
632 use case where you have acquired an OAuth 2.0 access_token from
633 another place such as a JavaScript client or another web
634 application, and wish to use it from Python. Because only the
635 access_token is present it can not be refreshed and will in time
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500636 expire.
637
Joe Gregorio9ce4b622011-02-17 15:32:11 -0500638 AccessTokenCredentials objects may be safely pickled and unpickled.
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500639
640 Usage:
641 credentials = AccessTokenCredentials('<an access token>',
642 'my-user-agent/1.0')
643 http = httplib2.Http()
644 http = credentials.authorize(http)
645
646 Exceptions:
647 AccessTokenCredentialsExpired: raised when the access_token expires or is
648 revoked.
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500649 """
650
651 def __init__(self, access_token, user_agent):
652 """Create an instance of OAuth2Credentials
653
654 This is one of the few types if Credentials that you should contrust,
655 Credentials objects are usually instantiated by a Flow.
656
657 Args:
ade@google.com93a7f7c2011-02-23 16:00:37 +0000658 access_token: string, access token.
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500659 user_agent: string, The HTTP User-Agent to provide for this application.
660
661 Notes:
662 store: callable, a callable that when passed a Credential
663 will store the credential back to where it came from.
664 """
665 super(AccessTokenCredentials, self).__init__(
666 access_token,
667 None,
668 None,
669 None,
670 None,
671 None,
672 user_agent)
673
Joe Gregorio562b7312011-09-15 09:06:38 -0400674
675 @classmethod
676 def from_json(cls, s):
677 data = simplejson.loads(s)
678 retval = AccessTokenCredentials(
679 data['access_token'],
680 data['user_agent'])
681 return retval
682
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500683 def _refresh(self, http_request):
684 raise AccessTokenCredentialsError(
685 "The access_token is expired or invalid and can't be refreshed.")
686
JacobMoshenko8e905102011-06-20 09:53:10 -0400687
688class AssertionCredentials(OAuth2Credentials):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400689 """Abstract Credentials object used for OAuth 2.0 assertion grants.
JacobMoshenko8e905102011-06-20 09:53:10 -0400690
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400691 This credential does not require a flow to instantiate because it
692 represents a two legged flow, and therefore has all of the required
693 information to generate and refresh its own access tokens. It must
694 be subclassed to generate the appropriate assertion string.
JacobMoshenko8e905102011-06-20 09:53:10 -0400695
696 AssertionCredentials objects may be safely pickled and unpickled.
697 """
698
699 def __init__(self, assertion_type, user_agent,
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400700 token_uri='https://accounts.google.com/o/oauth2/token',
701 **unused_kwargs):
702 """Constructor for AssertionFlowCredentials.
JacobMoshenko8e905102011-06-20 09:53:10 -0400703
704 Args:
705 assertion_type: string, assertion type that will be declared to the auth
706 server
707 user_agent: string, The HTTP User-Agent to provide for this application.
708 token_uri: string, URI for token endpoint. For convenience
709 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
710 """
711 super(AssertionCredentials, self).__init__(
712 None,
713 None,
714 None,
715 None,
716 None,
717 token_uri,
718 user_agent)
719 self.assertion_type = assertion_type
720
721 def _generate_refresh_request_body(self):
722 assertion = self._generate_assertion()
723
724 body = urllib.urlencode({
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400725 'assertion_type': self.assertion_type,
726 'assertion': assertion,
727 'grant_type': 'assertion',
728 })
JacobMoshenko8e905102011-06-20 09:53:10 -0400729
730 return body
731
732 def _generate_assertion(self):
733 """Generate the assertion string that will be used in the access token
734 request.
735 """
736 _abstract()
737
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500738if HAS_OPENSSL:
739 # PyOpenSSL is not a prerequisite for oauth2client, so if it is missing then
740 # don't create the SignedJwtAssertionCredentials or the verify_id_token()
741 # method.
742
743 class SignedJwtAssertionCredentials(AssertionCredentials):
744 """Credentials object used for OAuth 2.0 Signed JWT assertion grants.
745
746 This credential does not require a flow to instantiate because it
747 represents a two legged flow, and therefore has all of the required
748 information to generate and refresh its own access tokens.
749 """
750
751 MAX_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds
752
753 def __init__(self,
754 service_account_name,
755 private_key,
756 scope,
757 private_key_password='notasecret',
758 user_agent=None,
759 token_uri='https://accounts.google.com/o/oauth2/token',
760 **kwargs):
761 """Constructor for SignedJwtAssertionCredentials.
762
763 Args:
764 service_account_name: string, id for account, usually an email address.
765 private_key: string, private key in P12 format.
766 scope: string or list of strings, scope(s) of the credentials being
767 requested.
768 private_key_password: string, password for private_key.
769 user_agent: string, HTTP User-Agent to provide for this application.
770 token_uri: string, URI for token endpoint. For convenience
771 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
772 kwargs: kwargs, Additional parameters to add to the JWT token, for
773 example prn=joe@xample.org."""
774
775 super(SignedJwtAssertionCredentials, self).__init__(
776 'http://oauth.net/grant_type/jwt/1.0/bearer',
777 user_agent,
778 token_uri=token_uri,
779 )
780
781 if type(scope) is list:
782 scope = ' '.join(scope)
783 self.scope = scope
784
785 self.private_key = private_key
786 self.private_key_password = private_key_password
787 self.service_account_name = service_account_name
788 self.kwargs = kwargs
789
790 @classmethod
791 def from_json(cls, s):
792 data = simplejson.loads(s)
793 retval = SignedJwtAssertionCredentials(
794 data['service_account_name'],
795 data['private_key'],
796 data['private_key_password'],
797 data['scope'],
798 data['user_agent'],
799 data['token_uri'],
800 data['kwargs']
801 )
802 retval.invalid = data['invalid']
803 return retval
804
805 def _generate_assertion(self):
806 """Generate the assertion that will be used in the request."""
807 now = long(time.time())
808 payload = {
809 'aud': self.token_uri,
810 'scope': self.scope,
811 'iat': now,
812 'exp': now + SignedJwtAssertionCredentials.MAX_TOKEN_LIFETIME_SECS,
813 'iss': self.service_account_name
814 }
815 payload.update(self.kwargs)
Joe Gregorioe78621a2012-03-09 15:47:23 -0500816 logger.debug(str(payload))
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500817
818 return make_signed_jwt(
819 Signer.from_string(self.private_key, self.private_key_password),
820 payload)
821
Joe Gregorio9f2f38f2012-02-06 12:53:00 -0500822 # Only used in verify_id_token(), which is always calling to the same URI
823 # for the certs.
824 _cached_http = httplib2.Http(MemoryCache())
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500825
826 def verify_id_token(id_token, audience, http=None,
827 cert_uri=ID_TOKEN_VERIFICATON_CERTS):
828 """Verifies a signed JWT id_token.
829
830 Args:
831 id_token: string, A Signed JWT.
832 audience: string, The audience 'aud' that the token should be for.
833 http: httplib2.Http, instance to use to make the HTTP request. Callers
834 should supply an instance that has caching enabled.
835 cert_uri: string, URI of the certificates in JSON format to
836 verify the JWT against.
837
838 Returns:
839 The deserialized JSON in the JWT.
840
841 Raises:
842 oauth2client.crypt.AppIdentityError if the JWT fails to verify.
843 """
844 if http is None:
Joe Gregorio9f2f38f2012-02-06 12:53:00 -0500845 http = _cached_http
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500846
847 resp, content = http.request(cert_uri)
848
849 if resp.status == 200:
850 certs = simplejson.loads(content)
851 return verify_signed_jwt_with_certs(id_token, certs, audience)
852 else:
853 raise VerifyJwtTokenError('Status code: %d' % resp.status)
854
855
856def _urlsafe_b64decode(b64string):
Joe Gregoriobd512b52011-12-06 15:39:26 -0500857 # Guard against unicode strings, which base64 can't handle.
858 b64string = b64string.encode('ascii')
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500859 padded = b64string + '=' * (4 - len(b64string) % 4)
860 return base64.urlsafe_b64decode(padded)
861
862
863def _extract_id_token(id_token):
864 """Extract the JSON payload from a JWT.
865
866 Does the extraction w/o checking the signature.
867
868 Args:
869 id_token: string, OAuth 2.0 id_token.
870
871 Returns:
872 object, The deserialized JSON payload.
873 """
874 segments = id_token.split('.')
875
876 if (len(segments) != 3):
877 raise VerifyJwtTokenError(
878 'Wrong number of segments in token: %s' % id_token)
879
880 return simplejson.loads(_urlsafe_b64decode(segments[1]))
881
JacobMoshenko8e905102011-06-20 09:53:10 -0400882
Joe Gregorio695fdc12011-01-16 16:46:55 -0500883class OAuth2WebServerFlow(Flow):
884 """Does the Web Server Flow for OAuth 2.0.
885
886 OAuth2Credentials objects may be safely pickled and unpickled.
887 """
888
Joe Gregoriof08a4982011-10-07 13:11:16 -0400889 def __init__(self, client_id, client_secret, scope, user_agent=None,
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400890 auth_uri='https://accounts.google.com/o/oauth2/auth',
891 token_uri='https://accounts.google.com/o/oauth2/token',
892 **kwargs):
893 """Constructor for OAuth2WebServerFlow.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500894
895 Args:
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500896 client_id: string, client identifier.
897 client_secret: string client secret.
Joe Gregoriof2f8a5a2011-10-14 15:11:29 -0400898 scope: string or list of strings, scope(s) of the credentials being
899 requested.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500900 user_agent: string, HTTP User-Agent to provide for this application.
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500901 auth_uri: string, URI for authorization endpoint. For convenience
902 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
903 token_uri: string, URI for token endpoint. For convenience
904 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500905 **kwargs: dict, The keyword arguments are all optional and required
906 parameters for the OAuth calls.
907 """
908 self.client_id = client_id
909 self.client_secret = client_secret
Joe Gregoriof2f8a5a2011-10-14 15:11:29 -0400910 if type(scope) is list:
911 scope = ' '.join(scope)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500912 self.scope = scope
913 self.user_agent = user_agent
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500914 self.auth_uri = auth_uri
Joe Gregorio695fdc12011-01-16 16:46:55 -0500915 self.token_uri = token_uri
Joe Gregorio69a0aca2011-11-03 10:47:32 -0400916 self.params = {
917 'access_type': 'offline',
918 }
919 self.params.update(kwargs)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500920 self.redirect_uri = None
921
Joe Gregoriof2326c02012-02-09 12:18:44 -0500922 def step1_get_authorize_url(self, redirect_uri=OOB_CALLBACK_URN):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500923 """Returns a URI to redirect to the provider.
924
925 Args:
Joe Gregoriof2326c02012-02-09 12:18:44 -0500926 redirect_uri: string, Either the string 'urn:ietf:wg:oauth:2.0:oob' for
927 a non-web-based application, or a URI that handles the callback from
928 the authorization server.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500929
Joe Gregoriof2326c02012-02-09 12:18:44 -0500930 If redirect_uri is 'urn:ietf:wg:oauth:2.0:oob' then pass in the
Joe Gregorio695fdc12011-01-16 16:46:55 -0500931 generated verification code to step2_exchange,
932 otherwise pass in the query parameters received
933 at the callback uri to step2_exchange.
934 """
935
936 self.redirect_uri = redirect_uri
937 query = {
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400938 'response_type': 'code',
939 'client_id': self.client_id,
940 'redirect_uri': redirect_uri,
941 'scope': self.scope,
942 }
Joe Gregorio695fdc12011-01-16 16:46:55 -0500943 query.update(self.params)
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500944 parts = list(urlparse.urlparse(self.auth_uri))
Joe Gregorio695fdc12011-01-16 16:46:55 -0500945 query.update(dict(parse_qsl(parts[4]))) # 4 is the index of the query part
946 parts[4] = urllib.urlencode(query)
947 return urlparse.urlunparse(parts)
948
Joe Gregorioccc79542011-02-19 00:05:26 -0500949 def step2_exchange(self, code, http=None):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500950 """Exhanges a code for OAuth2Credentials.
951
952 Args:
953 code: string or dict, either the code as a string, or a dictionary
954 of the query parameters to the redirect_uri, which contains
955 the code.
Joe Gregorioccc79542011-02-19 00:05:26 -0500956 http: httplib2.Http, optional http instance to use to do the fetch
Joe Gregorio695fdc12011-01-16 16:46:55 -0500957 """
958
959 if not (isinstance(code, str) or isinstance(code, unicode)):
960 code = code['code']
961
962 body = urllib.urlencode({
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400963 'grant_type': 'authorization_code',
964 'client_id': self.client_id,
965 'client_secret': self.client_secret,
966 'code': code,
967 'redirect_uri': self.redirect_uri,
968 'scope': self.scope,
969 })
Joe Gregorio695fdc12011-01-16 16:46:55 -0500970 headers = {
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400971 'content-type': 'application/x-www-form-urlencoded',
Joe Gregorio695fdc12011-01-16 16:46:55 -0500972 }
JacobMoshenkocb6d8912011-07-08 13:35:15 -0400973
974 if self.user_agent is not None:
975 headers['user-agent'] = self.user_agent
976
Joe Gregorioccc79542011-02-19 00:05:26 -0500977 if http is None:
978 http = httplib2.Http()
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500979
JacobMoshenko8e905102011-06-20 09:53:10 -0400980 resp, content = http.request(self.token_uri, method='POST', body=body,
981 headers=headers)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500982 if resp.status == 200:
983 # TODO(jcgregorio) Raise an error if simplejson.loads fails?
984 d = simplejson.loads(content)
985 access_token = d['access_token']
986 refresh_token = d.get('refresh_token', None)
987 token_expiry = None
988 if 'expires_in' in d:
Joe Gregorio562b7312011-09-15 09:06:38 -0400989 token_expiry = datetime.datetime.utcnow() + datetime.timedelta(
JacobMoshenko8e905102011-06-20 09:53:10 -0400990 seconds=int(d['expires_in']))
Joe Gregorio695fdc12011-01-16 16:46:55 -0500991
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500992 if 'id_token' in d:
993 d['id_token'] = _extract_id_token(d['id_token'])
994
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400995 logger.info('Successfully retrieved access token: %s' % content)
JacobMoshenko8e905102011-06-20 09:53:10 -0400996 return OAuth2Credentials(access_token, self.client_id,
997 self.client_secret, refresh_token, token_expiry,
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500998 self.token_uri, self.user_agent,
999 id_token=d.get('id_token', None))
Joe Gregorio695fdc12011-01-16 16:46:55 -05001000 else:
Joe Gregorioe78621a2012-03-09 15:47:23 -05001001 logger.info('Failed to retrieve access token: %s' % content)
Joe Gregorioccc79542011-02-19 00:05:26 -05001002 error_msg = 'Invalid response %s.' % resp['status']
1003 try:
1004 d = simplejson.loads(content)
1005 if 'error' in d:
1006 error_msg = d['error']
1007 except:
1008 pass
1009
1010 raise FlowExchangeError(error_msg)
Joe Gregoriof08a4982011-10-07 13:11:16 -04001011
1012def flow_from_clientsecrets(filename, scope, message=None):
1013 """Create a Flow from a clientsecrets file.
1014
1015 Will create the right kind of Flow based on the contents of the clientsecrets
1016 file or will raise InvalidClientSecretsError for unknown types of Flows.
1017
1018 Args:
1019 filename: string, File name of client secrets.
Joe Gregoriof2f8a5a2011-10-14 15:11:29 -04001020 scope: string or list of strings, scope(s) to request.
Joe Gregoriof08a4982011-10-07 13:11:16 -04001021 message: string, A friendly string to display to the user if the
1022 clientsecrets file is missing or invalid. If message is provided then
1023 sys.exit will be called in the case of an error. If message in not
1024 provided then clientsecrets.InvalidClientSecretsError will be raised.
1025
1026 Returns:
1027 A Flow object.
1028
1029 Raises:
1030 UnknownClientSecretsFlowError if the file describes an unknown kind of Flow.
1031 clientsecrets.InvalidClientSecretsError if the clientsecrets file is
1032 invalid.
1033 """
Joe Gregorio0984ef22011-10-14 13:17:43 -04001034 try:
1035 client_type, client_info = clientsecrets.loadfile(filename)
1036 if client_type in [clientsecrets.TYPE_WEB, clientsecrets.TYPE_INSTALLED]:
1037 return OAuth2WebServerFlow(
1038 client_info['client_id'],
1039 client_info['client_secret'],
1040 scope,
1041 None, # user_agent
1042 client_info['auth_uri'],
1043 client_info['token_uri'])
1044 except clientsecrets.InvalidClientSecretsError:
1045 if message:
1046 sys.exit(message)
1047 else:
1048 raise
Joe Gregoriof08a4982011-10-07 13:11:16 -04001049 else:
1050 raise UnknownClientSecretsFlowError(
1051 'This OAuth 2.0 flow is unsupported: "%s"' * client_type)