blob: ce033ca8a4be40d63142cd72995b46b892bc671d [file] [log] [blame]
Joe Gregorio20a5aa92011-04-01 17:44:25 -04001# Copyright (C) 2010 Google Inc.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
Joe Gregorio695fdc12011-01-16 16:46:55 -050014
Joe Gregorio9da2ad82011-09-11 14:04:44 -040015"""An OAuth 2.0 client.
Joe Gregorio695fdc12011-01-16 16:46:55 -050016
Joe Gregorio9da2ad82011-09-11 14:04:44 -040017Tools for interacting with OAuth 2.0 protected resources.
Joe Gregorio695fdc12011-01-16 16:46:55 -050018"""
19
20__author__ = 'jcgregorio@google.com (Joe Gregorio)'
21
Joe Gregorio8b4c1732011-12-06 11:28:29 -050022import base64
Joe Gregoriof08a4982011-10-07 13:11:16 -040023import clientsecrets
Joe Gregorio695fdc12011-01-16 16:46:55 -050024import copy
25import datetime
26import httplib2
27import logging
Joe Gregorio8b4c1732011-12-06 11:28:29 -050028import os
Joe Gregoriof08a4982011-10-07 13:11:16 -040029import sys
Joe Gregorio8b4c1732011-12-06 11:28:29 -050030import time
Joe Gregorio695fdc12011-01-16 16:46:55 -050031import urllib
32import urlparse
33
Joe Gregorio549230c2012-01-11 10:38:05 -050034from anyjson import simplejson
Joe Gregorio8b4c1732011-12-06 11:28:29 -050035
36HAS_OPENSSL = False
37try:
38 from oauth2client.crypt import Signer
39 from oauth2client.crypt import make_signed_jwt
40 from oauth2client.crypt import verify_signed_jwt_with_certs
41 HAS_OPENSSL = True
42except ImportError:
43 pass
44
Joe Gregorio695fdc12011-01-16 16:46:55 -050045try:
Joe Gregorio9da2ad82011-09-11 14:04:44 -040046 from urlparse import parse_qsl
Joe Gregorio695fdc12011-01-16 16:46:55 -050047except ImportError:
Joe Gregorio9da2ad82011-09-11 14:04:44 -040048 from cgi import parse_qsl
49
50logger = logging.getLogger(__name__)
Joe Gregorio695fdc12011-01-16 16:46:55 -050051
Joe Gregorio562b7312011-09-15 09:06:38 -040052# Expiry is stored in RFC3339 UTC format
Joe Gregorio8b4c1732011-12-06 11:28:29 -050053EXPIRY_FORMAT = '%Y-%m-%dT%H:%M:%SZ'
54
55# Which certs to use to validate id_tokens received.
56ID_TOKEN_VERIFICATON_CERTS = 'https://www.googleapis.com/oauth2/v1/certs'
Joe Gregorio562b7312011-09-15 09:06:38 -040057
Joe Gregoriof2326c02012-02-09 12:18:44 -050058# Constant to use for the out of band OAuth 2.0 flow.
59OOB_CALLBACK_URN = 'urn:ietf:wg:oauth:2.0:oob'
60
Joe Gregorio695fdc12011-01-16 16:46:55 -050061
62class Error(Exception):
63 """Base error for this module."""
64 pass
65
66
Joe Gregorioccc79542011-02-19 00:05:26 -050067class FlowExchangeError(Error):
Joe Gregorioca876e42011-02-22 19:39:42 -050068 """Error trying to exchange an authorization grant for an access token."""
Joe Gregorioccc79542011-02-19 00:05:26 -050069 pass
70
71
72class AccessTokenRefreshError(Error):
Joe Gregorioca876e42011-02-22 19:39:42 -050073 """Error trying to refresh an expired access token."""
Joe Gregorio695fdc12011-01-16 16:46:55 -050074 pass
75
Joe Gregoriof08a4982011-10-07 13:11:16 -040076class UnknownClientSecretsFlowError(Error):
77 """The client secrets file called for an unknown type of OAuth 2.0 flow. """
78 pass
79
Joe Gregorio695fdc12011-01-16 16:46:55 -050080
Joe Gregorio3b79fa82011-02-17 11:47:17 -050081class AccessTokenCredentialsError(Error):
82 """Having only the access_token means no refresh is possible."""
Joe Gregorio695fdc12011-01-16 16:46:55 -050083 pass
84
85
Joe Gregorio8b4c1732011-12-06 11:28:29 -050086class VerifyJwtTokenError(Error):
87 """Could on retrieve certificates for validation."""
88 pass
89
90
Joe Gregorio695fdc12011-01-16 16:46:55 -050091def _abstract():
92 raise NotImplementedError('You need to override this function')
93
94
Joe Gregorio9f2f38f2012-02-06 12:53:00 -050095class MemoryCache(object):
96 """httplib2 Cache implementation which only caches locally."""
97
98 def __init__(self):
99 self.cache = {}
100
101 def get(self, key):
102 return self.cache.get(key)
103
104 def set(self, key, value):
105 self.cache[key] = value
106
107 def delete(self, key):
108 self.cache.pop(key, None)
109
110
Joe Gregorio695fdc12011-01-16 16:46:55 -0500111class Credentials(object):
112 """Base class for all Credentials objects.
113
Joe Gregorio562b7312011-09-15 09:06:38 -0400114 Subclasses must define an authorize() method that applies the credentials to
115 an HTTP transport.
116
117 Subclasses must also specify a classmethod named 'from_json' that takes a JSON
118 string as input and returns an instaniated Crentials object.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500119 """
120
Joe Gregorio562b7312011-09-15 09:06:38 -0400121 NON_SERIALIZED_MEMBERS = ['store']
122
Joe Gregorio695fdc12011-01-16 16:46:55 -0500123 def authorize(self, http):
124 """Take an httplib2.Http instance (or equivalent) and
125 authorizes it for the set of credentials, usually by
126 replacing http.request() with a method that adds in
127 the appropriate headers and then delegates to the original
128 Http.request() method.
129 """
130 _abstract()
131
Joe 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 Gregorioe9b40f12011-10-13 10:03:28 -0400194 m = __import__(module, fromlist=module.split('.')[:-1])
Joe Gregorio562b7312011-09-15 09:06:38 -0400195 kls = getattr(m, data['_class'])
196 from_json = getattr(kls, 'from_json')
197 return from_json(s)
198
JacobMoshenko8e905102011-06-20 09:53:10 -0400199
Joe Gregorio695fdc12011-01-16 16:46:55 -0500200class Flow(object):
201 """Base class for all Flow objects."""
202 pass
203
204
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500205class Storage(object):
206 """Base class for all Storage objects.
207
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400208 Store and retrieve a single credential. This class supports locking
209 such that multiple processes and threads can operate on a single
210 store.
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500211 """
212
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400213 def acquire_lock(self):
214 """Acquires any lock necessary to access this Storage.
215
216 This lock is not reentrant."""
217 pass
218
219 def release_lock(self):
220 """Release the Storage lock.
221
222 Trying to release a lock that isn't held will result in a
223 RuntimeError.
224 """
225 pass
226
227 def locked_get(self):
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500228 """Retrieve credential.
229
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400230 The Storage lock must be held when this is called.
231
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500232 Returns:
Joe Gregorio06d852b2011-03-25 15:03:10 -0400233 oauth2client.client.Credentials
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500234 """
235 _abstract()
236
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400237 def locked_put(self, credentials):
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500238 """Write a credential.
239
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400240 The Storage lock must be held when this is called.
241
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500242 Args:
243 credentials: Credentials, the credentials to store.
244 """
245 _abstract()
246
Joe Gregorioec75dc12012-02-06 13:40:42 -0500247 def locked_delete(self):
248 """Delete a credential.
249
250 The Storage lock must be held when this is called.
251 """
252 _abstract()
253
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400254 def get(self):
255 """Retrieve credential.
256
257 The Storage lock must *not* be held when this is called.
258
259 Returns:
260 oauth2client.client.Credentials
261 """
262 self.acquire_lock()
263 try:
264 return self.locked_get()
265 finally:
266 self.release_lock()
267
268 def put(self, credentials):
269 """Write a credential.
270
271 The Storage lock must be held when this is called.
272
273 Args:
274 credentials: Credentials, the credentials to store.
275 """
276 self.acquire_lock()
277 try:
278 self.locked_put(credentials)
279 finally:
280 self.release_lock()
281
Joe Gregorioec75dc12012-02-06 13:40:42 -0500282 def delete(self):
283 """Delete credential.
284
285 Frees any resources associated with storing the credential.
286 The Storage lock must *not* be held when this is called.
287
288 Returns:
289 None
290 """
291 self.acquire_lock()
292 try:
293 return self.locked_delete()
294 finally:
295 self.release_lock()
296
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500297
Joe Gregorio695fdc12011-01-16 16:46:55 -0500298class OAuth2Credentials(Credentials):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400299 """Credentials object for OAuth 2.0.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500300
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500301 Credentials can be applied to an httplib2.Http object using the authorize()
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500302 method, which then adds the OAuth 2.0 access token to each request.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500303
304 OAuth2Credentials objects may be safely pickled and unpickled.
305 """
306
307 def __init__(self, access_token, client_id, client_secret, refresh_token,
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500308 token_expiry, token_uri, user_agent, id_token=None):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400309 """Create an instance of OAuth2Credentials.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500310
311 This constructor is not usually called by the user, instead
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500312 OAuth2Credentials objects are instantiated by the OAuth2WebServerFlow.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500313
314 Args:
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400315 access_token: string, access token.
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500316 client_id: string, client identifier.
317 client_secret: string, client secret.
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500318 refresh_token: string, refresh token.
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400319 token_expiry: datetime, when the access_token expires.
320 token_uri: string, URI of token endpoint.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500321 user_agent: string, The HTTP User-Agent to provide for this application.
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500322 id_token: object, The identity of the resource owner.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500323
Joe Gregorio695fdc12011-01-16 16:46:55 -0500324 Notes:
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500325 store: callable, A callable that when passed a Credential
Joe Gregorio695fdc12011-01-16 16:46:55 -0500326 will store the credential back to where it came from.
327 This is needed to store the latest access_token if it
328 has expired and been refreshed.
329 """
330 self.access_token = access_token
331 self.client_id = client_id
332 self.client_secret = client_secret
333 self.refresh_token = refresh_token
334 self.store = None
335 self.token_expiry = token_expiry
336 self.token_uri = token_uri
337 self.user_agent = user_agent
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500338 self.id_token = id_token
Joe Gregorio695fdc12011-01-16 16:46:55 -0500339
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500340 # True if the credentials have been revoked or expired and can't be
341 # refreshed.
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400342 self.invalid = False
Joe Gregorio9ce4b622011-02-17 15:32:11 -0500343
Joe Gregorio654f4a22012-02-09 14:15:44 -0500344 def authorize(self, http):
345 """Authorize an httplib2.Http instance with these credentials.
346
347 The modified http.request method will add authentication headers to each
348 request and will refresh access_tokens when a 401 is received on a
349 request. In addition the http.request method has a credentials property,
350 http.request.credentials, which is the Credentials object that authorized
351 it.
352
353 Args:
354 http: An instance of httplib2.Http
355 or something that acts like it.
356
357 Returns:
358 A modified instance of http that was passed in.
359
360 Example:
361
362 h = httplib2.Http()
363 h = credentials.authorize(h)
364
365 You can't create a new OAuth subclass of httplib2.Authenication
366 because it never gets passed the absolute URI, which is needed for
367 signing. So instead we have to overload 'request' with a closure
368 that adds in the Authorization header and then calls the original
369 version of 'request()'.
370 """
371 request_orig = http.request
372
373 # The closure that will replace 'httplib2.Http.request'.
374 def new_request(uri, method='GET', body=None, headers=None,
375 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
376 connection_type=None):
377 if not self.access_token:
378 logger.info('Attempting refresh to obtain initial access_token')
379 self._refresh(request_orig)
380
381 # Modify the request headers to add the appropriate
382 # Authorization header.
383 if headers is None:
384 headers = {}
385 self.apply(headers)
386
387 if self.user_agent is not None:
388 if 'user-agent' in headers:
389 headers['user-agent'] = self.user_agent + ' ' + headers['user-agent']
390 else:
391 headers['user-agent'] = self.user_agent
392
393 resp, content = request_orig(uri, method, body, headers,
394 redirections, connection_type)
395
396 if resp.status == 401:
397 logger.info('Refreshing due to a 401')
398 self._refresh(request_orig)
399 self.apply(headers)
400 return request_orig(uri, method, body, headers,
401 redirections, connection_type)
402 else:
403 return (resp, content)
404
405 # Replace the request method with our own closure.
406 http.request = new_request
407
408 # Set credentials as a property of the request method.
409 setattr(http.request, 'credentials', self)
410
411 return http
412
413 def refresh(self, http):
414 """Forces a refresh of the access_token.
415
416 Args:
417 http: httplib2.Http, an http object to be used to make the refresh
418 request.
419 """
420 self._refresh(http.request)
421
422 def apply(self, headers):
423 """Add the authorization to the headers.
424
425 Args:
426 headers: dict, the headers to add the Authorization header to.
427 """
428 headers['Authorization'] = 'Bearer ' + self.access_token
429
Joe Gregorio562b7312011-09-15 09:06:38 -0400430 def to_json(self):
431 return self._to_json(Credentials.NON_SERIALIZED_MEMBERS)
432
433 @classmethod
434 def from_json(cls, s):
435 """Instantiate a Credentials object from a JSON description of it. The JSON
436 should have been produced by calling .to_json() on the object.
437
438 Args:
439 data: dict, A deserialized JSON object.
440
441 Returns:
442 An instance of a Credentials subclass.
443 """
444 data = simplejson.loads(s)
445 if 'token_expiry' in data and not isinstance(data['token_expiry'],
446 datetime.datetime):
Joe Gregorio1daa71b2011-09-15 18:12:14 -0400447 try:
448 data['token_expiry'] = datetime.datetime.strptime(
449 data['token_expiry'], EXPIRY_FORMAT)
450 except:
451 data['token_expiry'] = None
Joe Gregorio562b7312011-09-15 09:06:38 -0400452 retval = OAuth2Credentials(
453 data['access_token'],
454 data['client_id'],
455 data['client_secret'],
456 data['refresh_token'],
457 data['token_expiry'],
458 data['token_uri'],
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500459 data['user_agent'],
460 data.get('id_token', None))
Joe Gregorio562b7312011-09-15 09:06:38 -0400461 retval.invalid = data['invalid']
462 return retval
463
Joe Gregorio9ce4b622011-02-17 15:32:11 -0500464 @property
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400465 def access_token_expired(self):
466 """True if the credential is expired or invalid.
467
468 If the token_expiry isn't set, we assume the token doesn't expire.
469 """
470 if self.invalid:
471 return True
472
473 if not self.token_expiry:
474 return False
475
Joe Gregorio562b7312011-09-15 09:06:38 -0400476 now = datetime.datetime.utcnow()
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400477 if now >= self.token_expiry:
478 logger.info('access_token is expired. Now: %s, token_expiry: %s',
479 now, self.token_expiry)
480 return True
481 return False
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500482
Joe Gregorio695fdc12011-01-16 16:46:55 -0500483 def set_store(self, store):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400484 """Set the Storage for the credential.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500485
486 Args:
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400487 store: Storage, an implementation of Stroage object.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500488 This is needed to store the latest access_token if it
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400489 has expired and been refreshed. This implementation uses
490 locking to check for updates before updating the
491 access_token.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500492 """
493 self.store = store
494
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400495 def _updateFromCredential(self, other):
496 """Update this Credential from another instance."""
497 self.__dict__.update(other.__getstate__())
498
Joe Gregorio695fdc12011-01-16 16:46:55 -0500499 def __getstate__(self):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400500 """Trim the state down to something that can be pickled."""
Joe Gregorio695fdc12011-01-16 16:46:55 -0500501 d = copy.copy(self.__dict__)
502 del d['store']
503 return d
504
505 def __setstate__(self, state):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400506 """Reconstitute the state of the object from being pickled."""
Joe Gregorio695fdc12011-01-16 16:46:55 -0500507 self.__dict__.update(state)
508 self.store = None
509
JacobMoshenko8e905102011-06-20 09:53:10 -0400510 def _generate_refresh_request_body(self):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400511 """Generate the body that will be used in the refresh request."""
JacobMoshenko8e905102011-06-20 09:53:10 -0400512 body = urllib.urlencode({
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400513 'grant_type': 'refresh_token',
514 'client_id': self.client_id,
515 'client_secret': self.client_secret,
516 'refresh_token': self.refresh_token,
517 })
JacobMoshenko8e905102011-06-20 09:53:10 -0400518 return body
519
520 def _generate_refresh_request_headers(self):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400521 """Generate the headers that will be used in the refresh request."""
JacobMoshenko8e905102011-06-20 09:53:10 -0400522 headers = {
JacobMoshenko8e905102011-06-20 09:53:10 -0400523 'content-type': 'application/x-www-form-urlencoded',
524 }
JacobMoshenkocb6d8912011-07-08 13:35:15 -0400525
526 if self.user_agent is not None:
527 headers['user-agent'] = self.user_agent
528
JacobMoshenko8e905102011-06-20 09:53:10 -0400529 return headers
530
Joe Gregorio695fdc12011-01-16 16:46:55 -0500531 def _refresh(self, http_request):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400532 """Refreshes the access_token.
533
534 This method first checks by reading the Storage object if available.
535 If a refresh is still needed, it holds the Storage lock until the
536 refresh is completed.
Joe Gregorio654f4a22012-02-09 14:15:44 -0500537
538 Args:
539 http_request: callable, a callable that matches the method signature of
540 httplib2.Http.request, used to make the refresh request.
541
542 Raises:
543 AccessTokenRefreshError: When the refresh fails.
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400544 """
545 if not self.store:
546 self._do_refresh_request(http_request)
547 else:
548 self.store.acquire_lock()
549 try:
550 new_cred = self.store.locked_get()
551 if (new_cred and not new_cred.invalid and
552 new_cred.access_token != self.access_token):
553 logger.info('Updated access_token read from Storage')
554 self._updateFromCredential(new_cred)
555 else:
556 self._do_refresh_request(http_request)
557 finally:
558 self.store.release_lock()
559
560 def _do_refresh_request(self, http_request):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500561 """Refresh the access_token using the refresh_token.
562
563 Args:
Joe Gregorio654f4a22012-02-09 14:15:44 -0500564 http_request: callable, a callable that matches the method signature of
565 httplib2.Http.request, used to make the refresh request.
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400566
567 Raises:
568 AccessTokenRefreshError: When the refresh fails.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500569 """
JacobMoshenko8e905102011-06-20 09:53:10 -0400570 body = self._generate_refresh_request_body()
571 headers = self._generate_refresh_request_headers()
572
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400573 logger.info('Refresing access_token')
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500574 resp, content = http_request(
575 self.token_uri, method='POST', body=body, headers=headers)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500576 if resp.status == 200:
577 # TODO(jcgregorio) Raise an error if loads fails?
578 d = simplejson.loads(content)
579 self.access_token = d['access_token']
580 self.refresh_token = d.get('refresh_token', self.refresh_token)
581 if 'expires_in' in d:
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500582 self.token_expiry = datetime.timedelta(
Joe Gregorio562b7312011-09-15 09:06:38 -0400583 seconds=int(d['expires_in'])) + datetime.datetime.utcnow()
Joe Gregorio695fdc12011-01-16 16:46:55 -0500584 else:
585 self.token_expiry = None
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400586 if self.store:
587 self.store.locked_put(self)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500588 else:
JacobMoshenko8e905102011-06-20 09:53:10 -0400589 # An {'error':...} response body means the token is expired or revoked,
590 # so we flag the credentials as such.
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400591 logger.error('Failed to retrieve access token: %s' % content)
Joe Gregorioccc79542011-02-19 00:05:26 -0500592 error_msg = 'Invalid response %s.' % resp['status']
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500593 try:
594 d = simplejson.loads(content)
595 if 'error' in d:
Joe Gregorioccc79542011-02-19 00:05:26 -0500596 error_msg = d['error']
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400597 self.invalid = True
598 if self.store:
599 self.store.locked_put(self)
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500600 except:
601 pass
Joe Gregorioccc79542011-02-19 00:05:26 -0500602 raise AccessTokenRefreshError(error_msg)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500603
Joe Gregorio695fdc12011-01-16 16:46:55 -0500604
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500605class AccessTokenCredentials(OAuth2Credentials):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400606 """Credentials object for OAuth 2.0.
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500607
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400608 Credentials can be applied to an httplib2.Http object using the
609 authorize() method, which then signs each request from that object
610 with the OAuth 2.0 access token. This set of credentials is for the
611 use case where you have acquired an OAuth 2.0 access_token from
612 another place such as a JavaScript client or another web
613 application, and wish to use it from Python. Because only the
614 access_token is present it can not be refreshed and will in time
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500615 expire.
616
Joe Gregorio9ce4b622011-02-17 15:32:11 -0500617 AccessTokenCredentials objects may be safely pickled and unpickled.
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500618
619 Usage:
620 credentials = AccessTokenCredentials('<an access token>',
621 'my-user-agent/1.0')
622 http = httplib2.Http()
623 http = credentials.authorize(http)
624
625 Exceptions:
626 AccessTokenCredentialsExpired: raised when the access_token expires or is
627 revoked.
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500628 """
629
630 def __init__(self, access_token, user_agent):
631 """Create an instance of OAuth2Credentials
632
633 This is one of the few types if Credentials that you should contrust,
634 Credentials objects are usually instantiated by a Flow.
635
636 Args:
ade@google.com93a7f7c2011-02-23 16:00:37 +0000637 access_token: string, access token.
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500638 user_agent: string, The HTTP User-Agent to provide for this application.
639
640 Notes:
641 store: callable, a callable that when passed a Credential
642 will store the credential back to where it came from.
643 """
644 super(AccessTokenCredentials, self).__init__(
645 access_token,
646 None,
647 None,
648 None,
649 None,
650 None,
651 user_agent)
652
Joe Gregorio562b7312011-09-15 09:06:38 -0400653
654 @classmethod
655 def from_json(cls, s):
656 data = simplejson.loads(s)
657 retval = AccessTokenCredentials(
658 data['access_token'],
659 data['user_agent'])
660 return retval
661
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500662 def _refresh(self, http_request):
663 raise AccessTokenCredentialsError(
664 "The access_token is expired or invalid and can't be refreshed.")
665
JacobMoshenko8e905102011-06-20 09:53:10 -0400666
667class AssertionCredentials(OAuth2Credentials):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400668 """Abstract Credentials object used for OAuth 2.0 assertion grants.
JacobMoshenko8e905102011-06-20 09:53:10 -0400669
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400670 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. It must
673 be subclassed to generate the appropriate assertion string.
JacobMoshenko8e905102011-06-20 09:53:10 -0400674
675 AssertionCredentials objects may be safely pickled and unpickled.
676 """
677
678 def __init__(self, assertion_type, user_agent,
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400679 token_uri='https://accounts.google.com/o/oauth2/token',
680 **unused_kwargs):
681 """Constructor for AssertionFlowCredentials.
JacobMoshenko8e905102011-06-20 09:53:10 -0400682
683 Args:
684 assertion_type: string, assertion type that will be declared to the auth
685 server
686 user_agent: string, The HTTP User-Agent to provide for this application.
687 token_uri: string, URI for token endpoint. For convenience
688 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
689 """
690 super(AssertionCredentials, self).__init__(
691 None,
692 None,
693 None,
694 None,
695 None,
696 token_uri,
697 user_agent)
698 self.assertion_type = assertion_type
699
700 def _generate_refresh_request_body(self):
701 assertion = self._generate_assertion()
702
703 body = urllib.urlencode({
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400704 'assertion_type': self.assertion_type,
705 'assertion': assertion,
706 'grant_type': 'assertion',
707 })
JacobMoshenko8e905102011-06-20 09:53:10 -0400708
709 return body
710
711 def _generate_assertion(self):
712 """Generate the assertion string that will be used in the access token
713 request.
714 """
715 _abstract()
716
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500717if HAS_OPENSSL:
718 # PyOpenSSL is not a prerequisite for oauth2client, so if it is missing then
719 # don't create the SignedJwtAssertionCredentials or the verify_id_token()
720 # method.
721
722 class SignedJwtAssertionCredentials(AssertionCredentials):
723 """Credentials object used for OAuth 2.0 Signed JWT assertion grants.
724
725 This credential does not require a flow to instantiate because it
726 represents a two legged flow, and therefore has all of the required
727 information to generate and refresh its own access tokens.
728 """
729
730 MAX_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds
731
732 def __init__(self,
733 service_account_name,
734 private_key,
735 scope,
736 private_key_password='notasecret',
737 user_agent=None,
738 token_uri='https://accounts.google.com/o/oauth2/token',
739 **kwargs):
740 """Constructor for SignedJwtAssertionCredentials.
741
742 Args:
743 service_account_name: string, id for account, usually an email address.
744 private_key: string, private key in P12 format.
745 scope: string or list of strings, scope(s) of the credentials being
746 requested.
747 private_key_password: string, password for private_key.
748 user_agent: string, HTTP User-Agent to provide for this application.
749 token_uri: string, URI for token endpoint. For convenience
750 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
751 kwargs: kwargs, Additional parameters to add to the JWT token, for
752 example prn=joe@xample.org."""
753
754 super(SignedJwtAssertionCredentials, self).__init__(
755 'http://oauth.net/grant_type/jwt/1.0/bearer',
756 user_agent,
757 token_uri=token_uri,
758 )
759
760 if type(scope) is list:
761 scope = ' '.join(scope)
762 self.scope = scope
763
764 self.private_key = private_key
765 self.private_key_password = private_key_password
766 self.service_account_name = service_account_name
767 self.kwargs = kwargs
768
769 @classmethod
770 def from_json(cls, s):
771 data = simplejson.loads(s)
772 retval = SignedJwtAssertionCredentials(
773 data['service_account_name'],
774 data['private_key'],
775 data['private_key_password'],
776 data['scope'],
777 data['user_agent'],
778 data['token_uri'],
779 data['kwargs']
780 )
781 retval.invalid = data['invalid']
782 return retval
783
784 def _generate_assertion(self):
785 """Generate the assertion that will be used in the request."""
786 now = long(time.time())
787 payload = {
788 'aud': self.token_uri,
789 'scope': self.scope,
790 'iat': now,
791 'exp': now + SignedJwtAssertionCredentials.MAX_TOKEN_LIFETIME_SECS,
792 'iss': self.service_account_name
793 }
794 payload.update(self.kwargs)
795 logging.debug(str(payload))
796
797 return make_signed_jwt(
798 Signer.from_string(self.private_key, self.private_key_password),
799 payload)
800
Joe Gregorio9f2f38f2012-02-06 12:53:00 -0500801 # Only used in verify_id_token(), which is always calling to the same URI
802 # for the certs.
803 _cached_http = httplib2.Http(MemoryCache())
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500804
805 def verify_id_token(id_token, audience, http=None,
806 cert_uri=ID_TOKEN_VERIFICATON_CERTS):
807 """Verifies a signed JWT id_token.
808
809 Args:
810 id_token: string, A Signed JWT.
811 audience: string, The audience 'aud' that the token should be for.
812 http: httplib2.Http, instance to use to make the HTTP request. Callers
813 should supply an instance that has caching enabled.
814 cert_uri: string, URI of the certificates in JSON format to
815 verify the JWT against.
816
817 Returns:
818 The deserialized JSON in the JWT.
819
820 Raises:
821 oauth2client.crypt.AppIdentityError if the JWT fails to verify.
822 """
823 if http is None:
Joe Gregorio9f2f38f2012-02-06 12:53:00 -0500824 http = _cached_http
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500825
826 resp, content = http.request(cert_uri)
827
828 if resp.status == 200:
829 certs = simplejson.loads(content)
830 return verify_signed_jwt_with_certs(id_token, certs, audience)
831 else:
832 raise VerifyJwtTokenError('Status code: %d' % resp.status)
833
834
835def _urlsafe_b64decode(b64string):
Joe Gregoriobd512b52011-12-06 15:39:26 -0500836 # Guard against unicode strings, which base64 can't handle.
837 b64string = b64string.encode('ascii')
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500838 padded = b64string + '=' * (4 - len(b64string) % 4)
839 return base64.urlsafe_b64decode(padded)
840
841
842def _extract_id_token(id_token):
843 """Extract the JSON payload from a JWT.
844
845 Does the extraction w/o checking the signature.
846
847 Args:
848 id_token: string, OAuth 2.0 id_token.
849
850 Returns:
851 object, The deserialized JSON payload.
852 """
853 segments = id_token.split('.')
854
855 if (len(segments) != 3):
856 raise VerifyJwtTokenError(
857 'Wrong number of segments in token: %s' % id_token)
858
859 return simplejson.loads(_urlsafe_b64decode(segments[1]))
860
JacobMoshenko8e905102011-06-20 09:53:10 -0400861
Joe Gregorio695fdc12011-01-16 16:46:55 -0500862class OAuth2WebServerFlow(Flow):
863 """Does the Web Server Flow for OAuth 2.0.
864
865 OAuth2Credentials objects may be safely pickled and unpickled.
866 """
867
Joe Gregoriof08a4982011-10-07 13:11:16 -0400868 def __init__(self, client_id, client_secret, scope, user_agent=None,
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400869 auth_uri='https://accounts.google.com/o/oauth2/auth',
870 token_uri='https://accounts.google.com/o/oauth2/token',
871 **kwargs):
872 """Constructor for OAuth2WebServerFlow.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500873
874 Args:
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500875 client_id: string, client identifier.
876 client_secret: string client secret.
Joe Gregoriof2f8a5a2011-10-14 15:11:29 -0400877 scope: string or list of strings, scope(s) of the credentials being
878 requested.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500879 user_agent: string, HTTP User-Agent to provide for this application.
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500880 auth_uri: string, URI for authorization endpoint. For convenience
881 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
882 token_uri: string, URI for token endpoint. For convenience
883 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500884 **kwargs: dict, The keyword arguments are all optional and required
885 parameters for the OAuth calls.
886 """
887 self.client_id = client_id
888 self.client_secret = client_secret
Joe Gregoriof2f8a5a2011-10-14 15:11:29 -0400889 if type(scope) is list:
890 scope = ' '.join(scope)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500891 self.scope = scope
892 self.user_agent = user_agent
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500893 self.auth_uri = auth_uri
Joe Gregorio695fdc12011-01-16 16:46:55 -0500894 self.token_uri = token_uri
Joe Gregorio69a0aca2011-11-03 10:47:32 -0400895 self.params = {
896 'access_type': 'offline',
897 }
898 self.params.update(kwargs)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500899 self.redirect_uri = None
900
Joe Gregoriof2326c02012-02-09 12:18:44 -0500901 def step1_get_authorize_url(self, redirect_uri=OOB_CALLBACK_URN):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500902 """Returns a URI to redirect to the provider.
903
904 Args:
Joe Gregoriof2326c02012-02-09 12:18:44 -0500905 redirect_uri: string, Either the string 'urn:ietf:wg:oauth:2.0:oob' for
906 a non-web-based application, or a URI that handles the callback from
907 the authorization server.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500908
Joe Gregoriof2326c02012-02-09 12:18:44 -0500909 If redirect_uri is 'urn:ietf:wg:oauth:2.0:oob' then pass in the
Joe Gregorio695fdc12011-01-16 16:46:55 -0500910 generated verification code to step2_exchange,
911 otherwise pass in the query parameters received
912 at the callback uri to step2_exchange.
913 """
914
915 self.redirect_uri = redirect_uri
916 query = {
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400917 'response_type': 'code',
918 'client_id': self.client_id,
919 'redirect_uri': redirect_uri,
920 'scope': self.scope,
921 }
Joe Gregorio695fdc12011-01-16 16:46:55 -0500922 query.update(self.params)
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500923 parts = list(urlparse.urlparse(self.auth_uri))
Joe Gregorio695fdc12011-01-16 16:46:55 -0500924 query.update(dict(parse_qsl(parts[4]))) # 4 is the index of the query part
925 parts[4] = urllib.urlencode(query)
926 return urlparse.urlunparse(parts)
927
Joe Gregorioccc79542011-02-19 00:05:26 -0500928 def step2_exchange(self, code, http=None):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500929 """Exhanges a code for OAuth2Credentials.
930
931 Args:
932 code: string or dict, either the code as a string, or a dictionary
933 of the query parameters to the redirect_uri, which contains
934 the code.
Joe Gregorioccc79542011-02-19 00:05:26 -0500935 http: httplib2.Http, optional http instance to use to do the fetch
Joe Gregorio695fdc12011-01-16 16:46:55 -0500936 """
937
938 if not (isinstance(code, str) or isinstance(code, unicode)):
939 code = code['code']
940
941 body = urllib.urlencode({
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400942 'grant_type': 'authorization_code',
943 'client_id': self.client_id,
944 'client_secret': self.client_secret,
945 'code': code,
946 'redirect_uri': self.redirect_uri,
947 'scope': self.scope,
948 })
Joe Gregorio695fdc12011-01-16 16:46:55 -0500949 headers = {
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400950 'content-type': 'application/x-www-form-urlencoded',
Joe Gregorio695fdc12011-01-16 16:46:55 -0500951 }
JacobMoshenkocb6d8912011-07-08 13:35:15 -0400952
953 if self.user_agent is not None:
954 headers['user-agent'] = self.user_agent
955
Joe Gregorioccc79542011-02-19 00:05:26 -0500956 if http is None:
957 http = httplib2.Http()
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500958
JacobMoshenko8e905102011-06-20 09:53:10 -0400959 resp, content = http.request(self.token_uri, method='POST', body=body,
960 headers=headers)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500961 if resp.status == 200:
962 # TODO(jcgregorio) Raise an error if simplejson.loads fails?
963 d = simplejson.loads(content)
964 access_token = d['access_token']
965 refresh_token = d.get('refresh_token', None)
966 token_expiry = None
967 if 'expires_in' in d:
Joe Gregorio562b7312011-09-15 09:06:38 -0400968 token_expiry = datetime.datetime.utcnow() + datetime.timedelta(
JacobMoshenko8e905102011-06-20 09:53:10 -0400969 seconds=int(d['expires_in']))
Joe Gregorio695fdc12011-01-16 16:46:55 -0500970
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500971 if 'id_token' in d:
972 d['id_token'] = _extract_id_token(d['id_token'])
973
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400974 logger.info('Successfully retrieved access token: %s' % content)
JacobMoshenko8e905102011-06-20 09:53:10 -0400975 return OAuth2Credentials(access_token, self.client_id,
976 self.client_secret, refresh_token, token_expiry,
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500977 self.token_uri, self.user_agent,
978 id_token=d.get('id_token', None))
Joe Gregorio695fdc12011-01-16 16:46:55 -0500979 else:
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400980 logger.error('Failed to retrieve access token: %s' % content)
Joe Gregorioccc79542011-02-19 00:05:26 -0500981 error_msg = 'Invalid response %s.' % resp['status']
982 try:
983 d = simplejson.loads(content)
984 if 'error' in d:
985 error_msg = d['error']
986 except:
987 pass
988
989 raise FlowExchangeError(error_msg)
Joe Gregoriof08a4982011-10-07 13:11:16 -0400990
991def flow_from_clientsecrets(filename, scope, message=None):
992 """Create a Flow from a clientsecrets file.
993
994 Will create the right kind of Flow based on the contents of the clientsecrets
995 file or will raise InvalidClientSecretsError for unknown types of Flows.
996
997 Args:
998 filename: string, File name of client secrets.
Joe Gregoriof2f8a5a2011-10-14 15:11:29 -0400999 scope: string or list of strings, scope(s) to request.
Joe Gregoriof08a4982011-10-07 13:11:16 -04001000 message: string, A friendly string to display to the user if the
1001 clientsecrets file is missing or invalid. If message is provided then
1002 sys.exit will be called in the case of an error. If message in not
1003 provided then clientsecrets.InvalidClientSecretsError will be raised.
1004
1005 Returns:
1006 A Flow object.
1007
1008 Raises:
1009 UnknownClientSecretsFlowError if the file describes an unknown kind of Flow.
1010 clientsecrets.InvalidClientSecretsError if the clientsecrets file is
1011 invalid.
1012 """
Joe Gregorio0984ef22011-10-14 13:17:43 -04001013 try:
1014 client_type, client_info = clientsecrets.loadfile(filename)
1015 if client_type in [clientsecrets.TYPE_WEB, clientsecrets.TYPE_INSTALLED]:
1016 return OAuth2WebServerFlow(
1017 client_info['client_id'],
1018 client_info['client_secret'],
1019 scope,
1020 None, # user_agent
1021 client_info['auth_uri'],
1022 client_info['token_uri'])
1023 except clientsecrets.InvalidClientSecretsError:
1024 if message:
1025 sys.exit(message)
1026 else:
1027 raise
Joe Gregoriof08a4982011-10-07 13:11:16 -04001028 else:
1029 raise UnknownClientSecretsFlowError(
1030 'This OAuth 2.0 flow is unsupported: "%s"' * client_type)