blob: 71c9d81de18381f45e34672ded99a257dcb0b66b [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:
162 del d[member]
163 if 'token_expiry' in d and isinstance(d['token_expiry'], datetime.datetime):
164 d['token_expiry'] = d['token_expiry'].strftime(EXPIRY_FORMAT)
165 # Add in information we will need later to reconsistitue this instance.
166 d['_class'] = t.__name__
167 d['_module'] = t.__module__
168 return simplejson.dumps(d)
169
170 def to_json(self):
171 """Creating a JSON representation of an instance of Credentials.
172
173 Returns:
174 string, a JSON representation of this instance, suitable to pass to
175 from_json().
176 """
177 return self._to_json(Credentials.NON_SERIALIZED_MEMBERS)
178
179 @classmethod
180 def new_from_json(cls, s):
181 """Utility class method to instantiate a Credentials subclass from a JSON
182 representation produced by to_json().
183
184 Args:
185 s: string, JSON from to_json().
186
187 Returns:
188 An instance of the subclass of Credentials that was serialized with
189 to_json().
190 """
191 data = simplejson.loads(s)
192 # Find and call the right classmethod from_json() to restore the object.
193 module = data['_module']
Joe Gregoriofa8cd9f2012-02-23 14:00:40 -0500194 try:
195 m = __import__(module)
196 except ImportError:
197 # In case there's an object from the old package structure, update it
198 module = module.replace('.apiclient', '')
199 m = __import__(module)
200
Joe Gregorioe9b40f12011-10-13 10:03:28 -0400201 m = __import__(module, fromlist=module.split('.')[:-1])
Joe Gregorio562b7312011-09-15 09:06:38 -0400202 kls = getattr(m, data['_class'])
203 from_json = getattr(kls, 'from_json')
204 return from_json(s)
205
JacobMoshenko8e905102011-06-20 09:53:10 -0400206
Joe Gregorio695fdc12011-01-16 16:46:55 -0500207class Flow(object):
208 """Base class for all Flow objects."""
209 pass
210
211
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500212class Storage(object):
213 """Base class for all Storage objects.
214
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400215 Store and retrieve a single credential. This class supports locking
216 such that multiple processes and threads can operate on a single
217 store.
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500218 """
219
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400220 def acquire_lock(self):
221 """Acquires any lock necessary to access this Storage.
222
223 This lock is not reentrant."""
224 pass
225
226 def release_lock(self):
227 """Release the Storage lock.
228
229 Trying to release a lock that isn't held will result in a
230 RuntimeError.
231 """
232 pass
233
234 def locked_get(self):
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500235 """Retrieve credential.
236
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400237 The Storage lock must be held when this is called.
238
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500239 Returns:
Joe Gregorio06d852b2011-03-25 15:03:10 -0400240 oauth2client.client.Credentials
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500241 """
242 _abstract()
243
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400244 def locked_put(self, credentials):
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500245 """Write a credential.
246
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400247 The Storage lock must be held when this is called.
248
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500249 Args:
250 credentials: Credentials, the credentials to store.
251 """
252 _abstract()
253
Joe Gregorioec75dc12012-02-06 13:40:42 -0500254 def locked_delete(self):
255 """Delete a credential.
256
257 The Storage lock must be held when this is called.
258 """
259 _abstract()
260
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400261 def get(self):
262 """Retrieve credential.
263
264 The Storage lock must *not* be held when this is called.
265
266 Returns:
267 oauth2client.client.Credentials
268 """
269 self.acquire_lock()
270 try:
271 return self.locked_get()
272 finally:
273 self.release_lock()
274
275 def put(self, credentials):
276 """Write a credential.
277
278 The Storage lock must be held when this is called.
279
280 Args:
281 credentials: Credentials, the credentials to store.
282 """
283 self.acquire_lock()
284 try:
285 self.locked_put(credentials)
286 finally:
287 self.release_lock()
288
Joe Gregorioec75dc12012-02-06 13:40:42 -0500289 def delete(self):
290 """Delete credential.
291
292 Frees any resources associated with storing the credential.
293 The Storage lock must *not* be held when this is called.
294
295 Returns:
296 None
297 """
298 self.acquire_lock()
299 try:
300 return self.locked_delete()
301 finally:
302 self.release_lock()
303
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500304
Joe Gregorio695fdc12011-01-16 16:46:55 -0500305class OAuth2Credentials(Credentials):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400306 """Credentials object for OAuth 2.0.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500307
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500308 Credentials can be applied to an httplib2.Http object using the authorize()
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500309 method, which then adds the OAuth 2.0 access token to each request.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500310
311 OAuth2Credentials objects may be safely pickled and unpickled.
312 """
313
314 def __init__(self, access_token, client_id, client_secret, refresh_token,
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500315 token_expiry, token_uri, user_agent, id_token=None):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400316 """Create an instance of OAuth2Credentials.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500317
318 This constructor is not usually called by the user, instead
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500319 OAuth2Credentials objects are instantiated by the OAuth2WebServerFlow.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500320
321 Args:
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400322 access_token: string, access token.
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500323 client_id: string, client identifier.
324 client_secret: string, client secret.
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500325 refresh_token: string, refresh token.
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400326 token_expiry: datetime, when the access_token expires.
327 token_uri: string, URI of token endpoint.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500328 user_agent: string, The HTTP User-Agent to provide for this application.
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500329 id_token: object, The identity of the resource owner.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500330
Joe Gregorio695fdc12011-01-16 16:46:55 -0500331 Notes:
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500332 store: callable, A callable that when passed a Credential
Joe Gregorio695fdc12011-01-16 16:46:55 -0500333 will store the credential back to where it came from.
334 This is needed to store the latest access_token if it
335 has expired and been refreshed.
336 """
337 self.access_token = access_token
338 self.client_id = client_id
339 self.client_secret = client_secret
340 self.refresh_token = refresh_token
341 self.store = None
342 self.token_expiry = token_expiry
343 self.token_uri = token_uri
344 self.user_agent = user_agent
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500345 self.id_token = id_token
Joe Gregorio695fdc12011-01-16 16:46:55 -0500346
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500347 # True if the credentials have been revoked or expired and can't be
348 # refreshed.
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400349 self.invalid = False
Joe Gregorio9ce4b622011-02-17 15:32:11 -0500350
Joe Gregorio654f4a22012-02-09 14:15:44 -0500351 def authorize(self, http):
352 """Authorize an httplib2.Http instance with these credentials.
353
354 The modified http.request method will add authentication headers to each
355 request and will refresh access_tokens when a 401 is received on a
356 request. In addition the http.request method has a credentials property,
357 http.request.credentials, which is the Credentials object that authorized
358 it.
359
360 Args:
361 http: An instance of httplib2.Http
362 or something that acts like it.
363
364 Returns:
365 A modified instance of http that was passed in.
366
367 Example:
368
369 h = httplib2.Http()
370 h = credentials.authorize(h)
371
372 You can't create a new OAuth subclass of httplib2.Authenication
373 because it never gets passed the absolute URI, which is needed for
374 signing. So instead we have to overload 'request' with a closure
375 that adds in the Authorization header and then calls the original
376 version of 'request()'.
377 """
378 request_orig = http.request
379
380 # The closure that will replace 'httplib2.Http.request'.
381 def new_request(uri, method='GET', body=None, headers=None,
382 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
383 connection_type=None):
384 if not self.access_token:
385 logger.info('Attempting refresh to obtain initial access_token')
386 self._refresh(request_orig)
387
388 # Modify the request headers to add the appropriate
389 # Authorization header.
390 if headers is None:
391 headers = {}
392 self.apply(headers)
393
394 if self.user_agent is not None:
395 if 'user-agent' in headers:
396 headers['user-agent'] = self.user_agent + ' ' + headers['user-agent']
397 else:
398 headers['user-agent'] = self.user_agent
399
400 resp, content = request_orig(uri, method, body, headers,
401 redirections, connection_type)
402
403 if resp.status == 401:
404 logger.info('Refreshing due to a 401')
405 self._refresh(request_orig)
406 self.apply(headers)
407 return request_orig(uri, method, body, headers,
408 redirections, connection_type)
409 else:
410 return (resp, content)
411
412 # Replace the request method with our own closure.
413 http.request = new_request
414
415 # Set credentials as a property of the request method.
416 setattr(http.request, 'credentials', self)
417
418 return http
419
420 def refresh(self, http):
421 """Forces a refresh of the access_token.
422
423 Args:
424 http: httplib2.Http, an http object to be used to make the refresh
425 request.
426 """
427 self._refresh(http.request)
428
429 def apply(self, headers):
430 """Add the authorization to the headers.
431
432 Args:
433 headers: dict, the headers to add the Authorization header to.
434 """
435 headers['Authorization'] = 'Bearer ' + self.access_token
436
Joe Gregorio562b7312011-09-15 09:06:38 -0400437 def to_json(self):
438 return self._to_json(Credentials.NON_SERIALIZED_MEMBERS)
439
440 @classmethod
441 def from_json(cls, s):
442 """Instantiate a Credentials object from a JSON description of it. The JSON
443 should have been produced by calling .to_json() on the object.
444
445 Args:
446 data: dict, A deserialized JSON object.
447
448 Returns:
449 An instance of a Credentials subclass.
450 """
451 data = simplejson.loads(s)
452 if 'token_expiry' in data and not isinstance(data['token_expiry'],
453 datetime.datetime):
Joe Gregorio1daa71b2011-09-15 18:12:14 -0400454 try:
455 data['token_expiry'] = datetime.datetime.strptime(
456 data['token_expiry'], EXPIRY_FORMAT)
457 except:
458 data['token_expiry'] = None
Joe Gregorio562b7312011-09-15 09:06:38 -0400459 retval = OAuth2Credentials(
460 data['access_token'],
461 data['client_id'],
462 data['client_secret'],
463 data['refresh_token'],
464 data['token_expiry'],
465 data['token_uri'],
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500466 data['user_agent'],
467 data.get('id_token', None))
Joe Gregorio562b7312011-09-15 09:06:38 -0400468 retval.invalid = data['invalid']
469 return retval
470
Joe Gregorio9ce4b622011-02-17 15:32:11 -0500471 @property
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400472 def access_token_expired(self):
473 """True if the credential is expired or invalid.
474
475 If the token_expiry isn't set, we assume the token doesn't expire.
476 """
477 if self.invalid:
478 return True
479
480 if not self.token_expiry:
481 return False
482
Joe Gregorio562b7312011-09-15 09:06:38 -0400483 now = datetime.datetime.utcnow()
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400484 if now >= self.token_expiry:
485 logger.info('access_token is expired. Now: %s, token_expiry: %s',
486 now, self.token_expiry)
487 return True
488 return False
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500489
Joe Gregorio695fdc12011-01-16 16:46:55 -0500490 def set_store(self, store):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400491 """Set the Storage for the credential.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500492
493 Args:
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400494 store: Storage, an implementation of Stroage object.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500495 This is needed to store the latest access_token if it
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400496 has expired and been refreshed. This implementation uses
497 locking to check for updates before updating the
498 access_token.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500499 """
500 self.store = store
501
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400502 def _updateFromCredential(self, other):
503 """Update this Credential from another instance."""
504 self.__dict__.update(other.__getstate__())
505
Joe Gregorio695fdc12011-01-16 16:46:55 -0500506 def __getstate__(self):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400507 """Trim the state down to something that can be pickled."""
Joe Gregorio695fdc12011-01-16 16:46:55 -0500508 d = copy.copy(self.__dict__)
509 del d['store']
510 return d
511
512 def __setstate__(self, state):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400513 """Reconstitute the state of the object from being pickled."""
Joe Gregorio695fdc12011-01-16 16:46:55 -0500514 self.__dict__.update(state)
515 self.store = None
516
JacobMoshenko8e905102011-06-20 09:53:10 -0400517 def _generate_refresh_request_body(self):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400518 """Generate the body that will be used in the refresh request."""
JacobMoshenko8e905102011-06-20 09:53:10 -0400519 body = urllib.urlencode({
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400520 'grant_type': 'refresh_token',
521 'client_id': self.client_id,
522 'client_secret': self.client_secret,
523 'refresh_token': self.refresh_token,
524 })
JacobMoshenko8e905102011-06-20 09:53:10 -0400525 return body
526
527 def _generate_refresh_request_headers(self):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400528 """Generate the headers that will be used in the refresh request."""
JacobMoshenko8e905102011-06-20 09:53:10 -0400529 headers = {
JacobMoshenko8e905102011-06-20 09:53:10 -0400530 'content-type': 'application/x-www-form-urlencoded',
531 }
JacobMoshenkocb6d8912011-07-08 13:35:15 -0400532
533 if self.user_agent is not None:
534 headers['user-agent'] = self.user_agent
535
JacobMoshenko8e905102011-06-20 09:53:10 -0400536 return headers
537
Joe Gregorio695fdc12011-01-16 16:46:55 -0500538 def _refresh(self, http_request):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400539 """Refreshes the access_token.
540
541 This method first checks by reading the Storage object if available.
542 If a refresh is still needed, it holds the Storage lock until the
543 refresh is completed.
Joe Gregorio654f4a22012-02-09 14:15:44 -0500544
545 Args:
546 http_request: callable, a callable that matches the method signature of
547 httplib2.Http.request, used to make the refresh request.
548
549 Raises:
550 AccessTokenRefreshError: When the refresh fails.
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400551 """
552 if not self.store:
553 self._do_refresh_request(http_request)
554 else:
555 self.store.acquire_lock()
556 try:
557 new_cred = self.store.locked_get()
558 if (new_cred and not new_cred.invalid and
559 new_cred.access_token != self.access_token):
560 logger.info('Updated access_token read from Storage')
561 self._updateFromCredential(new_cred)
562 else:
563 self._do_refresh_request(http_request)
564 finally:
565 self.store.release_lock()
566
567 def _do_refresh_request(self, http_request):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500568 """Refresh the access_token using the refresh_token.
569
570 Args:
Joe Gregorio654f4a22012-02-09 14:15:44 -0500571 http_request: callable, a callable that matches the method signature of
572 httplib2.Http.request, used to make the refresh request.
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400573
574 Raises:
575 AccessTokenRefreshError: When the refresh fails.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500576 """
JacobMoshenko8e905102011-06-20 09:53:10 -0400577 body = self._generate_refresh_request_body()
578 headers = self._generate_refresh_request_headers()
579
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400580 logger.info('Refresing access_token')
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500581 resp, content = http_request(
582 self.token_uri, method='POST', body=body, headers=headers)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500583 if resp.status == 200:
584 # TODO(jcgregorio) Raise an error if loads fails?
585 d = simplejson.loads(content)
586 self.access_token = d['access_token']
587 self.refresh_token = d.get('refresh_token', self.refresh_token)
588 if 'expires_in' in d:
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500589 self.token_expiry = datetime.timedelta(
Joe Gregorio562b7312011-09-15 09:06:38 -0400590 seconds=int(d['expires_in'])) + datetime.datetime.utcnow()
Joe Gregorio695fdc12011-01-16 16:46:55 -0500591 else:
592 self.token_expiry = None
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400593 if self.store:
594 self.store.locked_put(self)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500595 else:
JacobMoshenko8e905102011-06-20 09:53:10 -0400596 # An {'error':...} response body means the token is expired or revoked,
597 # so we flag the credentials as such.
Joe Gregorioe78621a2012-03-09 15:47:23 -0500598 logger.info('Failed to retrieve access token: %s' % content)
Joe Gregorioccc79542011-02-19 00:05:26 -0500599 error_msg = 'Invalid response %s.' % resp['status']
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500600 try:
601 d = simplejson.loads(content)
602 if 'error' in d:
Joe Gregorioccc79542011-02-19 00:05:26 -0500603 error_msg = d['error']
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400604 self.invalid = True
605 if self.store:
606 self.store.locked_put(self)
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500607 except:
608 pass
Joe Gregorioccc79542011-02-19 00:05:26 -0500609 raise AccessTokenRefreshError(error_msg)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500610
Joe Gregorio695fdc12011-01-16 16:46:55 -0500611
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500612class AccessTokenCredentials(OAuth2Credentials):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400613 """Credentials object for OAuth 2.0.
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500614
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400615 Credentials can be applied to an httplib2.Http object using the
616 authorize() method, which then signs each request from that object
617 with the OAuth 2.0 access token. This set of credentials is for the
618 use case where you have acquired an OAuth 2.0 access_token from
619 another place such as a JavaScript client or another web
620 application, and wish to use it from Python. Because only the
621 access_token is present it can not be refreshed and will in time
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500622 expire.
623
Joe Gregorio9ce4b622011-02-17 15:32:11 -0500624 AccessTokenCredentials objects may be safely pickled and unpickled.
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500625
626 Usage:
627 credentials = AccessTokenCredentials('<an access token>',
628 'my-user-agent/1.0')
629 http = httplib2.Http()
630 http = credentials.authorize(http)
631
632 Exceptions:
633 AccessTokenCredentialsExpired: raised when the access_token expires or is
634 revoked.
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500635 """
636
637 def __init__(self, access_token, user_agent):
638 """Create an instance of OAuth2Credentials
639
640 This is one of the few types if Credentials that you should contrust,
641 Credentials objects are usually instantiated by a Flow.
642
643 Args:
ade@google.com93a7f7c2011-02-23 16:00:37 +0000644 access_token: string, access token.
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500645 user_agent: string, The HTTP User-Agent to provide for this application.
646
647 Notes:
648 store: callable, a callable that when passed a Credential
649 will store the credential back to where it came from.
650 """
651 super(AccessTokenCredentials, self).__init__(
652 access_token,
653 None,
654 None,
655 None,
656 None,
657 None,
658 user_agent)
659
Joe Gregorio562b7312011-09-15 09:06:38 -0400660
661 @classmethod
662 def from_json(cls, s):
663 data = simplejson.loads(s)
664 retval = AccessTokenCredentials(
665 data['access_token'],
666 data['user_agent'])
667 return retval
668
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500669 def _refresh(self, http_request):
670 raise AccessTokenCredentialsError(
671 "The access_token is expired or invalid and can't be refreshed.")
672
JacobMoshenko8e905102011-06-20 09:53:10 -0400673
674class AssertionCredentials(OAuth2Credentials):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400675 """Abstract Credentials object used for OAuth 2.0 assertion grants.
JacobMoshenko8e905102011-06-20 09:53:10 -0400676
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400677 This credential does not require a flow to instantiate because it
678 represents a two legged flow, and therefore has all of the required
679 information to generate and refresh its own access tokens. It must
680 be subclassed to generate the appropriate assertion string.
JacobMoshenko8e905102011-06-20 09:53:10 -0400681
682 AssertionCredentials objects may be safely pickled and unpickled.
683 """
684
685 def __init__(self, assertion_type, user_agent,
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400686 token_uri='https://accounts.google.com/o/oauth2/token',
687 **unused_kwargs):
688 """Constructor for AssertionFlowCredentials.
JacobMoshenko8e905102011-06-20 09:53:10 -0400689
690 Args:
691 assertion_type: string, assertion type that will be declared to the auth
692 server
693 user_agent: string, The 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 """
697 super(AssertionCredentials, self).__init__(
698 None,
699 None,
700 None,
701 None,
702 None,
703 token_uri,
704 user_agent)
705 self.assertion_type = assertion_type
706
707 def _generate_refresh_request_body(self):
708 assertion = self._generate_assertion()
709
710 body = urllib.urlencode({
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400711 'assertion_type': self.assertion_type,
712 'assertion': assertion,
713 'grant_type': 'assertion',
714 })
JacobMoshenko8e905102011-06-20 09:53:10 -0400715
716 return body
717
718 def _generate_assertion(self):
719 """Generate the assertion string that will be used in the access token
720 request.
721 """
722 _abstract()
723
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500724if HAS_OPENSSL:
725 # PyOpenSSL is not a prerequisite for oauth2client, so if it is missing then
726 # don't create the SignedJwtAssertionCredentials or the verify_id_token()
727 # method.
728
729 class SignedJwtAssertionCredentials(AssertionCredentials):
730 """Credentials object used for OAuth 2.0 Signed JWT assertion grants.
731
732 This credential does not require a flow to instantiate because it
733 represents a two legged flow, and therefore has all of the required
734 information to generate and refresh its own access tokens.
735 """
736
737 MAX_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds
738
739 def __init__(self,
740 service_account_name,
741 private_key,
742 scope,
743 private_key_password='notasecret',
744 user_agent=None,
745 token_uri='https://accounts.google.com/o/oauth2/token',
746 **kwargs):
747 """Constructor for SignedJwtAssertionCredentials.
748
749 Args:
750 service_account_name: string, id for account, usually an email address.
751 private_key: string, private key in P12 format.
752 scope: string or list of strings, scope(s) of the credentials being
753 requested.
754 private_key_password: string, password for private_key.
755 user_agent: string, HTTP User-Agent to provide for this application.
756 token_uri: string, URI for token endpoint. For convenience
757 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
758 kwargs: kwargs, Additional parameters to add to the JWT token, for
759 example prn=joe@xample.org."""
760
761 super(SignedJwtAssertionCredentials, self).__init__(
762 'http://oauth.net/grant_type/jwt/1.0/bearer',
763 user_agent,
764 token_uri=token_uri,
765 )
766
767 if type(scope) is list:
768 scope = ' '.join(scope)
769 self.scope = scope
770
771 self.private_key = private_key
772 self.private_key_password = private_key_password
773 self.service_account_name = service_account_name
774 self.kwargs = kwargs
775
776 @classmethod
777 def from_json(cls, s):
778 data = simplejson.loads(s)
779 retval = SignedJwtAssertionCredentials(
780 data['service_account_name'],
781 data['private_key'],
782 data['private_key_password'],
783 data['scope'],
784 data['user_agent'],
785 data['token_uri'],
786 data['kwargs']
787 )
788 retval.invalid = data['invalid']
789 return retval
790
791 def _generate_assertion(self):
792 """Generate the assertion that will be used in the request."""
793 now = long(time.time())
794 payload = {
795 'aud': self.token_uri,
796 'scope': self.scope,
797 'iat': now,
798 'exp': now + SignedJwtAssertionCredentials.MAX_TOKEN_LIFETIME_SECS,
799 'iss': self.service_account_name
800 }
801 payload.update(self.kwargs)
Joe Gregorioe78621a2012-03-09 15:47:23 -0500802 logger.debug(str(payload))
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500803
804 return make_signed_jwt(
805 Signer.from_string(self.private_key, self.private_key_password),
806 payload)
807
Joe Gregorio9f2f38f2012-02-06 12:53:00 -0500808 # Only used in verify_id_token(), which is always calling to the same URI
809 # for the certs.
810 _cached_http = httplib2.Http(MemoryCache())
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500811
812 def verify_id_token(id_token, audience, http=None,
813 cert_uri=ID_TOKEN_VERIFICATON_CERTS):
814 """Verifies a signed JWT id_token.
815
816 Args:
817 id_token: string, A Signed JWT.
818 audience: string, The audience 'aud' that the token should be for.
819 http: httplib2.Http, instance to use to make the HTTP request. Callers
820 should supply an instance that has caching enabled.
821 cert_uri: string, URI of the certificates in JSON format to
822 verify the JWT against.
823
824 Returns:
825 The deserialized JSON in the JWT.
826
827 Raises:
828 oauth2client.crypt.AppIdentityError if the JWT fails to verify.
829 """
830 if http is None:
Joe Gregorio9f2f38f2012-02-06 12:53:00 -0500831 http = _cached_http
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500832
833 resp, content = http.request(cert_uri)
834
835 if resp.status == 200:
836 certs = simplejson.loads(content)
837 return verify_signed_jwt_with_certs(id_token, certs, audience)
838 else:
839 raise VerifyJwtTokenError('Status code: %d' % resp.status)
840
841
842def _urlsafe_b64decode(b64string):
Joe Gregoriobd512b52011-12-06 15:39:26 -0500843 # Guard against unicode strings, which base64 can't handle.
844 b64string = b64string.encode('ascii')
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500845 padded = b64string + '=' * (4 - len(b64string) % 4)
846 return base64.urlsafe_b64decode(padded)
847
848
849def _extract_id_token(id_token):
850 """Extract the JSON payload from a JWT.
851
852 Does the extraction w/o checking the signature.
853
854 Args:
855 id_token: string, OAuth 2.0 id_token.
856
857 Returns:
858 object, The deserialized JSON payload.
859 """
860 segments = id_token.split('.')
861
862 if (len(segments) != 3):
863 raise VerifyJwtTokenError(
864 'Wrong number of segments in token: %s' % id_token)
865
866 return simplejson.loads(_urlsafe_b64decode(segments[1]))
867
JacobMoshenko8e905102011-06-20 09:53:10 -0400868
Joe Gregorio695fdc12011-01-16 16:46:55 -0500869class OAuth2WebServerFlow(Flow):
870 """Does the Web Server Flow for OAuth 2.0.
871
872 OAuth2Credentials objects may be safely pickled and unpickled.
873 """
874
Joe Gregoriof08a4982011-10-07 13:11:16 -0400875 def __init__(self, client_id, client_secret, scope, user_agent=None,
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400876 auth_uri='https://accounts.google.com/o/oauth2/auth',
877 token_uri='https://accounts.google.com/o/oauth2/token',
878 **kwargs):
879 """Constructor for OAuth2WebServerFlow.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500880
881 Args:
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500882 client_id: string, client identifier.
883 client_secret: string client secret.
Joe Gregoriof2f8a5a2011-10-14 15:11:29 -0400884 scope: string or list of strings, scope(s) of the credentials being
885 requested.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500886 user_agent: string, HTTP User-Agent to provide for this application.
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500887 auth_uri: string, URI for authorization endpoint. For convenience
888 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
889 token_uri: string, URI for token endpoint. For convenience
890 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500891 **kwargs: dict, The keyword arguments are all optional and required
892 parameters for the OAuth calls.
893 """
894 self.client_id = client_id
895 self.client_secret = client_secret
Joe Gregoriof2f8a5a2011-10-14 15:11:29 -0400896 if type(scope) is list:
897 scope = ' '.join(scope)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500898 self.scope = scope
899 self.user_agent = user_agent
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500900 self.auth_uri = auth_uri
Joe Gregorio695fdc12011-01-16 16:46:55 -0500901 self.token_uri = token_uri
Joe Gregorio69a0aca2011-11-03 10:47:32 -0400902 self.params = {
903 'access_type': 'offline',
904 }
905 self.params.update(kwargs)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500906 self.redirect_uri = None
907
Joe Gregoriof2326c02012-02-09 12:18:44 -0500908 def step1_get_authorize_url(self, redirect_uri=OOB_CALLBACK_URN):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500909 """Returns a URI to redirect to the provider.
910
911 Args:
Joe Gregoriof2326c02012-02-09 12:18:44 -0500912 redirect_uri: string, Either the string 'urn:ietf:wg:oauth:2.0:oob' for
913 a non-web-based application, or a URI that handles the callback from
914 the authorization server.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500915
Joe Gregoriof2326c02012-02-09 12:18:44 -0500916 If redirect_uri is 'urn:ietf:wg:oauth:2.0:oob' then pass in the
Joe Gregorio695fdc12011-01-16 16:46:55 -0500917 generated verification code to step2_exchange,
918 otherwise pass in the query parameters received
919 at the callback uri to step2_exchange.
920 """
921
922 self.redirect_uri = redirect_uri
923 query = {
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400924 'response_type': 'code',
925 'client_id': self.client_id,
926 'redirect_uri': redirect_uri,
927 'scope': self.scope,
928 }
Joe Gregorio695fdc12011-01-16 16:46:55 -0500929 query.update(self.params)
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500930 parts = list(urlparse.urlparse(self.auth_uri))
Joe Gregorio695fdc12011-01-16 16:46:55 -0500931 query.update(dict(parse_qsl(parts[4]))) # 4 is the index of the query part
932 parts[4] = urllib.urlencode(query)
933 return urlparse.urlunparse(parts)
934
Joe Gregorioccc79542011-02-19 00:05:26 -0500935 def step2_exchange(self, code, http=None):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500936 """Exhanges a code for OAuth2Credentials.
937
938 Args:
939 code: string or dict, either the code as a string, or a dictionary
940 of the query parameters to the redirect_uri, which contains
941 the code.
Joe Gregorioccc79542011-02-19 00:05:26 -0500942 http: httplib2.Http, optional http instance to use to do the fetch
Joe Gregorio695fdc12011-01-16 16:46:55 -0500943 """
944
945 if not (isinstance(code, str) or isinstance(code, unicode)):
946 code = code['code']
947
948 body = urllib.urlencode({
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400949 'grant_type': 'authorization_code',
950 'client_id': self.client_id,
951 'client_secret': self.client_secret,
952 'code': code,
953 'redirect_uri': self.redirect_uri,
954 'scope': self.scope,
955 })
Joe Gregorio695fdc12011-01-16 16:46:55 -0500956 headers = {
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400957 'content-type': 'application/x-www-form-urlencoded',
Joe Gregorio695fdc12011-01-16 16:46:55 -0500958 }
JacobMoshenkocb6d8912011-07-08 13:35:15 -0400959
960 if self.user_agent is not None:
961 headers['user-agent'] = self.user_agent
962
Joe Gregorioccc79542011-02-19 00:05:26 -0500963 if http is None:
964 http = httplib2.Http()
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500965
JacobMoshenko8e905102011-06-20 09:53:10 -0400966 resp, content = http.request(self.token_uri, method='POST', body=body,
967 headers=headers)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500968 if resp.status == 200:
969 # TODO(jcgregorio) Raise an error if simplejson.loads fails?
970 d = simplejson.loads(content)
971 access_token = d['access_token']
972 refresh_token = d.get('refresh_token', None)
973 token_expiry = None
974 if 'expires_in' in d:
Joe Gregorio562b7312011-09-15 09:06:38 -0400975 token_expiry = datetime.datetime.utcnow() + datetime.timedelta(
JacobMoshenko8e905102011-06-20 09:53:10 -0400976 seconds=int(d['expires_in']))
Joe Gregorio695fdc12011-01-16 16:46:55 -0500977
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500978 if 'id_token' in d:
979 d['id_token'] = _extract_id_token(d['id_token'])
980
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400981 logger.info('Successfully retrieved access token: %s' % content)
JacobMoshenko8e905102011-06-20 09:53:10 -0400982 return OAuth2Credentials(access_token, self.client_id,
983 self.client_secret, refresh_token, token_expiry,
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500984 self.token_uri, self.user_agent,
985 id_token=d.get('id_token', None))
Joe Gregorio695fdc12011-01-16 16:46:55 -0500986 else:
Joe Gregorioe78621a2012-03-09 15:47:23 -0500987 logger.info('Failed to retrieve access token: %s' % content)
Joe Gregorioccc79542011-02-19 00:05:26 -0500988 error_msg = 'Invalid response %s.' % resp['status']
989 try:
990 d = simplejson.loads(content)
991 if 'error' in d:
992 error_msg = d['error']
993 except:
994 pass
995
996 raise FlowExchangeError(error_msg)
Joe Gregoriof08a4982011-10-07 13:11:16 -0400997
998def flow_from_clientsecrets(filename, scope, message=None):
999 """Create a Flow from a clientsecrets file.
1000
1001 Will create the right kind of Flow based on the contents of the clientsecrets
1002 file or will raise InvalidClientSecretsError for unknown types of Flows.
1003
1004 Args:
1005 filename: string, File name of client secrets.
Joe Gregoriof2f8a5a2011-10-14 15:11:29 -04001006 scope: string or list of strings, scope(s) to request.
Joe Gregoriof08a4982011-10-07 13:11:16 -04001007 message: string, A friendly string to display to the user if the
1008 clientsecrets file is missing or invalid. If message is provided then
1009 sys.exit will be called in the case of an error. If message in not
1010 provided then clientsecrets.InvalidClientSecretsError will be raised.
1011
1012 Returns:
1013 A Flow object.
1014
1015 Raises:
1016 UnknownClientSecretsFlowError if the file describes an unknown kind of Flow.
1017 clientsecrets.InvalidClientSecretsError if the clientsecrets file is
1018 invalid.
1019 """
Joe Gregorio0984ef22011-10-14 13:17:43 -04001020 try:
1021 client_type, client_info = clientsecrets.loadfile(filename)
1022 if client_type in [clientsecrets.TYPE_WEB, clientsecrets.TYPE_INSTALLED]:
1023 return OAuth2WebServerFlow(
1024 client_info['client_id'],
1025 client_info['client_secret'],
1026 scope,
1027 None, # user_agent
1028 client_info['auth_uri'],
1029 client_info['token_uri'])
1030 except clientsecrets.InvalidClientSecretsError:
1031 if message:
1032 sys.exit(message)
1033 else:
1034 raise
Joe Gregoriof08a4982011-10-07 13:11:16 -04001035 else:
1036 raise UnknownClientSecretsFlowError(
1037 'This OAuth 2.0 flow is unsupported: "%s"' * client_type)