blob: 1ad94e6d778eda86c1f8a2978db35cee9697ff6c [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
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -080034from oauth2client import GOOGLE_AUTH_URI
35from oauth2client import GOOGLE_REVOKE_URI
36from oauth2client import GOOGLE_TOKEN_URI
Joe Gregorio68a8cfe2012-08-03 16:17:40 -040037from oauth2client import util
38from oauth2client.anyjson import simplejson
Joe Gregorio8b4c1732011-12-06 11:28:29 -050039
Joe Gregorio0b723c22013-01-03 15:00:50 -050040HAS_CRYPTO = False
Joe Gregorio8b4c1732011-12-06 11:28:29 -050041try:
Joe Gregorio0b723c22013-01-03 15:00:50 -050042 from oauth2client import crypt
43 HAS_CRYPTO = True
Joe Gregorio8b4c1732011-12-06 11:28:29 -050044except ImportError:
45 pass
46
Joe Gregorio695fdc12011-01-16 16:46:55 -050047try:
Joe Gregorio9da2ad82011-09-11 14:04:44 -040048 from urlparse import parse_qsl
Joe Gregorio695fdc12011-01-16 16:46:55 -050049except ImportError:
Joe Gregorio9da2ad82011-09-11 14:04:44 -040050 from cgi import parse_qsl
51
52logger = logging.getLogger(__name__)
Joe Gregorio695fdc12011-01-16 16:46:55 -050053
Joe Gregorio562b7312011-09-15 09:06:38 -040054# Expiry is stored in RFC3339 UTC format
Joe Gregorio8b4c1732011-12-06 11:28:29 -050055EXPIRY_FORMAT = '%Y-%m-%dT%H:%M:%SZ'
56
57# Which certs to use to validate id_tokens received.
58ID_TOKEN_VERIFICATON_CERTS = 'https://www.googleapis.com/oauth2/v1/certs'
Joe Gregorio562b7312011-09-15 09:06:38 -040059
Joe Gregoriof2326c02012-02-09 12:18:44 -050060# Constant to use for the out of band OAuth 2.0 flow.
61OOB_CALLBACK_URN = 'urn:ietf:wg:oauth:2.0:oob'
62
Joe Gregorio0bd8c412013-01-03 17:17:46 -050063# Google Data client libraries may need to set this to [401, 403].
64REFRESH_STATUS_CODES = [401]
65
Joe Gregorio695fdc12011-01-16 16:46:55 -050066
67class Error(Exception):
68 """Base error for this module."""
Joe Gregorio695fdc12011-01-16 16:46:55 -050069
70
Joe Gregorioccc79542011-02-19 00:05:26 -050071class FlowExchangeError(Error):
Joe Gregorioca876e42011-02-22 19:39:42 -050072 """Error trying to exchange an authorization grant for an access token."""
Joe Gregorioccc79542011-02-19 00:05:26 -050073
74
75class AccessTokenRefreshError(Error):
Joe Gregorioca876e42011-02-22 19:39:42 -050076 """Error trying to refresh an expired access token."""
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -080077
78
79class TokenRevokeError(Error):
80 """Error trying to revoke a token."""
81
Joe Gregorio695fdc12011-01-16 16:46:55 -050082
Joe Gregoriof08a4982011-10-07 13:11:16 -040083class UnknownClientSecretsFlowError(Error):
84 """The client secrets file called for an unknown type of OAuth 2.0 flow. """
Joe Gregoriof08a4982011-10-07 13:11:16 -040085
Joe Gregorio695fdc12011-01-16 16:46:55 -050086
Joe Gregorio3b79fa82011-02-17 11:47:17 -050087class AccessTokenCredentialsError(Error):
88 """Having only the access_token means no refresh is possible."""
Joe Gregorio695fdc12011-01-16 16:46:55 -050089
90
Joe Gregorio8b4c1732011-12-06 11:28:29 -050091class VerifyJwtTokenError(Error):
92 """Could on retrieve certificates for validation."""
Joe Gregorio8b4c1732011-12-06 11:28:29 -050093
94
Joe Gregorio83f2ee62012-12-06 15:25:54 -050095class NonAsciiHeaderError(Error):
96 """Header names and values must be ASCII strings."""
Joe Gregorio83f2ee62012-12-06 15:25:54 -050097
98
Joe Gregorio695fdc12011-01-16 16:46:55 -050099def _abstract():
100 raise NotImplementedError('You need to override this function')
101
102
Joe Gregorio9f2f38f2012-02-06 12:53:00 -0500103class MemoryCache(object):
104 """httplib2 Cache implementation which only caches locally."""
105
106 def __init__(self):
107 self.cache = {}
108
109 def get(self, key):
110 return self.cache.get(key)
111
112 def set(self, key, value):
113 self.cache[key] = value
114
115 def delete(self, key):
116 self.cache.pop(key, None)
117
118
Joe Gregorio695fdc12011-01-16 16:46:55 -0500119class Credentials(object):
120 """Base class for all Credentials objects.
121
Joe Gregorio562b7312011-09-15 09:06:38 -0400122 Subclasses must define an authorize() method that applies the credentials to
123 an HTTP transport.
124
125 Subclasses must also specify a classmethod named 'from_json' that takes a JSON
Joe Gregoriofa8cd9f2012-02-23 14:00:40 -0500126 string as input and returns an instaniated Credentials object.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500127 """
128
Joe Gregorio562b7312011-09-15 09:06:38 -0400129 NON_SERIALIZED_MEMBERS = ['store']
130
Joe Gregorio695fdc12011-01-16 16:46:55 -0500131 def authorize(self, http):
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800132 """Take an httplib2.Http instance (or equivalent) and authorizes it.
133
134 Authorizes it for the set of credentials, usually by replacing
135 http.request() with a method that adds in the appropriate headers and then
136 delegates to the original Http.request() method.
137
138 Args:
139 http: httplib2.Http, an http object to be used to make the refresh
140 request.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500141 """
142 _abstract()
143
Joe Gregorio654f4a22012-02-09 14:15:44 -0500144 def refresh(self, http):
145 """Forces a refresh of the access_token.
146
147 Args:
148 http: httplib2.Http, an http object to be used to make the refresh
149 request.
150 """
151 _abstract()
152
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800153 def revoke(self, http):
154 """Revokes a refresh_token and makes the credentials void.
155
156 Args:
157 http: httplib2.Http, an http object to be used to make the revoke
158 request.
159 """
160 _abstract()
161
Joe Gregorio654f4a22012-02-09 14:15:44 -0500162 def apply(self, headers):
163 """Add the authorization to the headers.
164
165 Args:
166 headers: dict, the headers to add the Authorization header to.
167 """
168 _abstract()
169
Joe Gregorio562b7312011-09-15 09:06:38 -0400170 def _to_json(self, strip):
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800171 """Utility function that creates JSON repr. of a Credentials object.
Joe Gregorio562b7312011-09-15 09:06:38 -0400172
173 Args:
174 strip: array, An array of names of members to not include in the JSON.
175
176 Returns:
177 string, a JSON representation of this instance, suitable to pass to
178 from_json().
179 """
180 t = type(self)
181 d = copy.copy(self.__dict__)
182 for member in strip:
Joe Gregorio08cdcb82012-03-14 00:09:33 -0400183 if member in d:
184 del d[member]
Joe Gregorio562b7312011-09-15 09:06:38 -0400185 if 'token_expiry' in d and isinstance(d['token_expiry'], datetime.datetime):
186 d['token_expiry'] = d['token_expiry'].strftime(EXPIRY_FORMAT)
187 # Add in information we will need later to reconsistitue this instance.
188 d['_class'] = t.__name__
189 d['_module'] = t.__module__
190 return simplejson.dumps(d)
191
192 def to_json(self):
193 """Creating a JSON representation of an instance of Credentials.
194
195 Returns:
196 string, a JSON representation of this instance, suitable to pass to
197 from_json().
198 """
199 return self._to_json(Credentials.NON_SERIALIZED_MEMBERS)
200
201 @classmethod
202 def new_from_json(cls, s):
203 """Utility class method to instantiate a Credentials subclass from a JSON
204 representation produced by to_json().
205
206 Args:
207 s: string, JSON from to_json().
208
209 Returns:
210 An instance of the subclass of Credentials that was serialized with
211 to_json().
212 """
213 data = simplejson.loads(s)
214 # Find and call the right classmethod from_json() to restore the object.
215 module = data['_module']
Joe Gregoriofa8cd9f2012-02-23 14:00:40 -0500216 try:
217 m = __import__(module)
218 except ImportError:
219 # In case there's an object from the old package structure, update it
220 module = module.replace('.apiclient', '')
221 m = __import__(module)
222
Joe Gregorioe9b40f12011-10-13 10:03:28 -0400223 m = __import__(module, fromlist=module.split('.')[:-1])
Joe Gregorio562b7312011-09-15 09:06:38 -0400224 kls = getattr(m, data['_class'])
225 from_json = getattr(kls, 'from_json')
226 return from_json(s)
227
Joe Gregorio08cdcb82012-03-14 00:09:33 -0400228 @classmethod
229 def from_json(cls, s):
Joe Gregorio401b8422012-05-03 16:35:35 -0400230 """Instantiate a Credentials object from a JSON description of it.
231
232 The JSON should have been produced by calling .to_json() on the object.
Joe Gregorio08cdcb82012-03-14 00:09:33 -0400233
234 Args:
235 data: dict, A deserialized JSON object.
236
237 Returns:
238 An instance of a Credentials subclass.
239 """
240 return Credentials()
241
JacobMoshenko8e905102011-06-20 09:53:10 -0400242
Joe Gregorio695fdc12011-01-16 16:46:55 -0500243class Flow(object):
244 """Base class for all Flow objects."""
245 pass
246
247
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500248class Storage(object):
249 """Base class for all Storage objects.
250
Joe Gregorioe2233cd2013-01-24 15:46:23 -0500251 Store and retrieve a single credential. This class supports locking
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400252 such that multiple processes and threads can operate on a single
253 store.
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500254 """
255
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400256 def acquire_lock(self):
257 """Acquires any lock necessary to access this Storage.
258
Joe Gregorio401b8422012-05-03 16:35:35 -0400259 This lock is not reentrant.
260 """
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400261 pass
262
263 def release_lock(self):
264 """Release the Storage lock.
265
266 Trying to release a lock that isn't held will result in a
267 RuntimeError.
268 """
269 pass
270
271 def locked_get(self):
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500272 """Retrieve credential.
273
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400274 The Storage lock must be held when this is called.
275
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500276 Returns:
Joe Gregorio06d852b2011-03-25 15:03:10 -0400277 oauth2client.client.Credentials
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500278 """
279 _abstract()
280
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400281 def locked_put(self, credentials):
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500282 """Write a credential.
283
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400284 The Storage lock must be held when this is called.
285
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500286 Args:
287 credentials: Credentials, the credentials to store.
288 """
289 _abstract()
290
Joe Gregorioec75dc12012-02-06 13:40:42 -0500291 def locked_delete(self):
292 """Delete a credential.
293
294 The Storage lock must be held when this is called.
295 """
296 _abstract()
297
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400298 def get(self):
299 """Retrieve credential.
300
301 The Storage lock must *not* be held when this is called.
302
303 Returns:
304 oauth2client.client.Credentials
305 """
306 self.acquire_lock()
307 try:
308 return self.locked_get()
309 finally:
310 self.release_lock()
311
312 def put(self, credentials):
313 """Write a credential.
314
315 The Storage lock must be held when this is called.
316
317 Args:
318 credentials: Credentials, the credentials to store.
319 """
320 self.acquire_lock()
321 try:
322 self.locked_put(credentials)
323 finally:
324 self.release_lock()
325
Joe Gregorioec75dc12012-02-06 13:40:42 -0500326 def delete(self):
327 """Delete credential.
328
329 Frees any resources associated with storing the credential.
330 The Storage lock must *not* be held when this is called.
331
332 Returns:
333 None
334 """
335 self.acquire_lock()
336 try:
337 return self.locked_delete()
338 finally:
339 self.release_lock()
340
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500341
Joe Gregorio83f2ee62012-12-06 15:25:54 -0500342def clean_headers(headers):
343 """Forces header keys and values to be strings, i.e not unicode.
344
345 The httplib module just concats the header keys and values in a way that may
346 make the message header a unicode string, which, if it then tries to
347 contatenate to a binary request body may result in a unicode decode error.
348
349 Args:
350 headers: dict, A dictionary of headers.
351
352 Returns:
353 The same dictionary but with all the keys converted to strings.
354 """
355 clean = {}
356 try:
357 for k, v in headers.iteritems():
358 clean[str(k)] = str(v)
359 except UnicodeEncodeError:
360 raise NonAsciiHeaderError(k + ': ' + v)
361 return clean
362
363
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800364def _update_query_params(uri, params):
365 """Updates a URI with new query parameters.
366
367 Args:
368 uri: string, A valid URI, with potential existing query parameters.
369 params: dict, A dictionary of query parameters.
370
371 Returns:
372 The same URI but with the new query parameters added.
373 """
374 parts = list(urlparse.urlparse(uri))
375 query_params = dict(parse_qsl(parts[4])) # 4 is the index of the query part
376 query_params.update(params)
377 parts[4] = urllib.urlencode(query_params)
378 return urlparse.urlunparse(parts)
379
380
Joe Gregorio695fdc12011-01-16 16:46:55 -0500381class OAuth2Credentials(Credentials):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400382 """Credentials object for OAuth 2.0.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500383
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500384 Credentials can be applied to an httplib2.Http object using the authorize()
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500385 method, which then adds the OAuth 2.0 access token to each request.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500386
387 OAuth2Credentials objects may be safely pickled and unpickled.
388 """
389
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400390 @util.positional(8)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500391 def __init__(self, access_token, client_id, client_secret, refresh_token,
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800392 token_expiry, token_uri, user_agent, revoke_uri=None,
393 id_token=None):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400394 """Create an instance of OAuth2Credentials.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500395
396 This constructor is not usually called by the user, instead
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500397 OAuth2Credentials objects are instantiated by the OAuth2WebServerFlow.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500398
399 Args:
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400400 access_token: string, access token.
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500401 client_id: string, client identifier.
402 client_secret: string, client secret.
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500403 refresh_token: string, refresh token.
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400404 token_expiry: datetime, when the access_token expires.
405 token_uri: string, URI of token endpoint.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500406 user_agent: string, The HTTP User-Agent to provide for this application.
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800407 revoke_uri: string, URI for revoke endpoint. Defaults to None; a token
408 can't be revoked if this is None.
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500409 id_token: object, The identity of the resource owner.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500410
Joe Gregorio695fdc12011-01-16 16:46:55 -0500411 Notes:
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500412 store: callable, A callable that when passed a Credential
Joe Gregorio695fdc12011-01-16 16:46:55 -0500413 will store the credential back to where it came from.
414 This is needed to store the latest access_token if it
415 has expired and been refreshed.
416 """
417 self.access_token = access_token
418 self.client_id = client_id
419 self.client_secret = client_secret
420 self.refresh_token = refresh_token
421 self.store = None
422 self.token_expiry = token_expiry
423 self.token_uri = token_uri
424 self.user_agent = user_agent
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800425 self.revoke_uri = revoke_uri
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500426 self.id_token = id_token
Joe Gregorio695fdc12011-01-16 16:46:55 -0500427
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500428 # True if the credentials have been revoked or expired and can't be
429 # refreshed.
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400430 self.invalid = False
Joe Gregorio9ce4b622011-02-17 15:32:11 -0500431
Joe Gregorio654f4a22012-02-09 14:15:44 -0500432 def authorize(self, http):
433 """Authorize an httplib2.Http instance with these credentials.
434
435 The modified http.request method will add authentication headers to each
436 request and will refresh access_tokens when a 401 is received on a
437 request. In addition the http.request method has a credentials property,
438 http.request.credentials, which is the Credentials object that authorized
439 it.
440
441 Args:
442 http: An instance of httplib2.Http
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800443 or something that acts like it.
Joe Gregorio654f4a22012-02-09 14:15:44 -0500444
445 Returns:
446 A modified instance of http that was passed in.
447
448 Example:
449
450 h = httplib2.Http()
451 h = credentials.authorize(h)
452
453 You can't create a new OAuth subclass of httplib2.Authenication
454 because it never gets passed the absolute URI, which is needed for
455 signing. So instead we have to overload 'request' with a closure
456 that adds in the Authorization header and then calls the original
457 version of 'request()'.
458 """
459 request_orig = http.request
460
461 # The closure that will replace 'httplib2.Http.request'.
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400462 @util.positional(1)
Joe Gregorio654f4a22012-02-09 14:15:44 -0500463 def new_request(uri, method='GET', body=None, headers=None,
464 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
465 connection_type=None):
466 if not self.access_token:
467 logger.info('Attempting refresh to obtain initial access_token')
468 self._refresh(request_orig)
469
470 # Modify the request headers to add the appropriate
471 # Authorization header.
472 if headers is None:
473 headers = {}
474 self.apply(headers)
475
476 if self.user_agent is not None:
477 if 'user-agent' in headers:
478 headers['user-agent'] = self.user_agent + ' ' + headers['user-agent']
479 else:
480 headers['user-agent'] = self.user_agent
481
Joe Gregorio83f2ee62012-12-06 15:25:54 -0500482 resp, content = request_orig(uri, method, body, clean_headers(headers),
Joe Gregorio654f4a22012-02-09 14:15:44 -0500483 redirections, connection_type)
484
Joe Gregorio0bd8c412013-01-03 17:17:46 -0500485 if resp.status in REFRESH_STATUS_CODES:
Joe Gregorio7c7c6b12012-07-16 16:31:01 -0400486 logger.info('Refreshing due to a %s' % str(resp.status))
Joe Gregorio654f4a22012-02-09 14:15:44 -0500487 self._refresh(request_orig)
488 self.apply(headers)
Joe Gregorio83f2ee62012-12-06 15:25:54 -0500489 return request_orig(uri, method, body, clean_headers(headers),
Joe Gregorio654f4a22012-02-09 14:15:44 -0500490 redirections, connection_type)
491 else:
492 return (resp, content)
493
494 # Replace the request method with our own closure.
495 http.request = new_request
496
497 # Set credentials as a property of the request method.
498 setattr(http.request, 'credentials', self)
499
500 return http
501
502 def refresh(self, http):
503 """Forces a refresh of the access_token.
504
505 Args:
506 http: httplib2.Http, an http object to be used to make the refresh
507 request.
508 """
509 self._refresh(http.request)
510
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800511 def revoke(self, http):
512 """Revokes a refresh_token and makes the credentials void.
513
514 Args:
515 http: httplib2.Http, an http object to be used to make the revoke
516 request.
517 """
518 self._revoke(http.request)
519
Joe Gregorio654f4a22012-02-09 14:15:44 -0500520 def apply(self, headers):
521 """Add the authorization to the headers.
522
523 Args:
524 headers: dict, the headers to add the Authorization header to.
525 """
526 headers['Authorization'] = 'Bearer ' + self.access_token
527
Joe Gregorio562b7312011-09-15 09:06:38 -0400528 def to_json(self):
529 return self._to_json(Credentials.NON_SERIALIZED_MEMBERS)
530
531 @classmethod
532 def from_json(cls, s):
533 """Instantiate a Credentials object from a JSON description of it. The JSON
534 should have been produced by calling .to_json() on the object.
535
536 Args:
537 data: dict, A deserialized JSON object.
538
539 Returns:
540 An instance of a Credentials subclass.
541 """
542 data = simplejson.loads(s)
543 if 'token_expiry' in data and not isinstance(data['token_expiry'],
544 datetime.datetime):
Joe Gregorio1daa71b2011-09-15 18:12:14 -0400545 try:
546 data['token_expiry'] = datetime.datetime.strptime(
547 data['token_expiry'], EXPIRY_FORMAT)
548 except:
549 data['token_expiry'] = None
Joe Gregorio562b7312011-09-15 09:06:38 -0400550 retval = OAuth2Credentials(
551 data['access_token'],
552 data['client_id'],
553 data['client_secret'],
554 data['refresh_token'],
555 data['token_expiry'],
556 data['token_uri'],
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500557 data['user_agent'],
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800558 revoke_uri=data.get('revoke_uri', None),
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400559 id_token=data.get('id_token', None))
Joe Gregorio562b7312011-09-15 09:06:38 -0400560 retval.invalid = data['invalid']
561 return retval
562
Joe Gregorio9ce4b622011-02-17 15:32:11 -0500563 @property
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400564 def access_token_expired(self):
565 """True if the credential is expired or invalid.
566
567 If the token_expiry isn't set, we assume the token doesn't expire.
568 """
569 if self.invalid:
570 return True
571
572 if not self.token_expiry:
573 return False
574
Joe Gregorio562b7312011-09-15 09:06:38 -0400575 now = datetime.datetime.utcnow()
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400576 if now >= self.token_expiry:
577 logger.info('access_token is expired. Now: %s, token_expiry: %s',
578 now, self.token_expiry)
579 return True
580 return False
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500581
Joe Gregorio695fdc12011-01-16 16:46:55 -0500582 def set_store(self, store):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400583 """Set the Storage for the credential.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500584
585 Args:
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400586 store: Storage, an implementation of Stroage object.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500587 This is needed to store the latest access_token if it
Joe Gregorioe2233cd2013-01-24 15:46:23 -0500588 has expired and been refreshed. This implementation uses
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400589 locking to check for updates before updating the
590 access_token.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500591 """
592 self.store = store
593
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400594 def _updateFromCredential(self, other):
595 """Update this Credential from another instance."""
596 self.__dict__.update(other.__getstate__())
597
Joe Gregorio695fdc12011-01-16 16:46:55 -0500598 def __getstate__(self):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400599 """Trim the state down to something that can be pickled."""
Joe Gregorio695fdc12011-01-16 16:46:55 -0500600 d = copy.copy(self.__dict__)
601 del d['store']
602 return d
603
604 def __setstate__(self, state):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400605 """Reconstitute the state of the object from being pickled."""
Joe Gregorio695fdc12011-01-16 16:46:55 -0500606 self.__dict__.update(state)
607 self.store = None
608
JacobMoshenko8e905102011-06-20 09:53:10 -0400609 def _generate_refresh_request_body(self):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400610 """Generate the body that will be used in the refresh request."""
JacobMoshenko8e905102011-06-20 09:53:10 -0400611 body = urllib.urlencode({
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400612 'grant_type': 'refresh_token',
613 'client_id': self.client_id,
614 'client_secret': self.client_secret,
615 'refresh_token': self.refresh_token,
616 })
JacobMoshenko8e905102011-06-20 09:53:10 -0400617 return body
618
619 def _generate_refresh_request_headers(self):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400620 """Generate the headers that will be used in the refresh request."""
JacobMoshenko8e905102011-06-20 09:53:10 -0400621 headers = {
JacobMoshenko8e905102011-06-20 09:53:10 -0400622 'content-type': 'application/x-www-form-urlencoded',
623 }
JacobMoshenkocb6d8912011-07-08 13:35:15 -0400624
625 if self.user_agent is not None:
626 headers['user-agent'] = self.user_agent
627
JacobMoshenko8e905102011-06-20 09:53:10 -0400628 return headers
629
Joe Gregorio695fdc12011-01-16 16:46:55 -0500630 def _refresh(self, http_request):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400631 """Refreshes the access_token.
632
633 This method first checks by reading the Storage object if available.
634 If a refresh is still needed, it holds the Storage lock until the
635 refresh is completed.
Joe Gregorio654f4a22012-02-09 14:15:44 -0500636
637 Args:
638 http_request: callable, a callable that matches the method signature of
639 httplib2.Http.request, used to make the refresh request.
640
641 Raises:
642 AccessTokenRefreshError: When the refresh fails.
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400643 """
644 if not self.store:
645 self._do_refresh_request(http_request)
646 else:
647 self.store.acquire_lock()
648 try:
649 new_cred = self.store.locked_get()
650 if (new_cred and not new_cred.invalid and
651 new_cred.access_token != self.access_token):
652 logger.info('Updated access_token read from Storage')
653 self._updateFromCredential(new_cred)
654 else:
655 self._do_refresh_request(http_request)
656 finally:
657 self.store.release_lock()
658
659 def _do_refresh_request(self, http_request):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500660 """Refresh the access_token using the refresh_token.
661
662 Args:
Joe Gregorio654f4a22012-02-09 14:15:44 -0500663 http_request: callable, a callable that matches the method signature of
664 httplib2.Http.request, used to make the refresh request.
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400665
666 Raises:
667 AccessTokenRefreshError: When the refresh fails.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500668 """
JacobMoshenko8e905102011-06-20 09:53:10 -0400669 body = self._generate_refresh_request_body()
670 headers = self._generate_refresh_request_headers()
671
Joe Gregorio4b4002f2012-06-14 15:41:01 -0400672 logger.info('Refreshing access_token')
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500673 resp, content = http_request(
674 self.token_uri, method='POST', body=body, headers=headers)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500675 if resp.status == 200:
676 # TODO(jcgregorio) Raise an error if loads fails?
677 d = simplejson.loads(content)
678 self.access_token = d['access_token']
679 self.refresh_token = d.get('refresh_token', self.refresh_token)
680 if 'expires_in' in d:
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500681 self.token_expiry = datetime.timedelta(
Joe Gregorio562b7312011-09-15 09:06:38 -0400682 seconds=int(d['expires_in'])) + datetime.datetime.utcnow()
Joe Gregorio695fdc12011-01-16 16:46:55 -0500683 else:
684 self.token_expiry = None
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400685 if self.store:
686 self.store.locked_put(self)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500687 else:
JacobMoshenko8e905102011-06-20 09:53:10 -0400688 # An {'error':...} response body means the token is expired or revoked,
689 # so we flag the credentials as such.
Joe Gregorioe78621a2012-03-09 15:47:23 -0500690 logger.info('Failed to retrieve access token: %s' % content)
Joe Gregorioccc79542011-02-19 00:05:26 -0500691 error_msg = 'Invalid response %s.' % resp['status']
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500692 try:
693 d = simplejson.loads(content)
694 if 'error' in d:
Joe Gregorioccc79542011-02-19 00:05:26 -0500695 error_msg = d['error']
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400696 self.invalid = True
697 if self.store:
698 self.store.locked_put(self)
Joe Gregoriofd08e432012-08-09 14:17:41 -0400699 except StandardError:
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500700 pass
Joe Gregorioccc79542011-02-19 00:05:26 -0500701 raise AccessTokenRefreshError(error_msg)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500702
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800703 def _revoke(self, http_request):
704 """Revokes the refresh_token and deletes the store if available.
705
706 Args:
707 http_request: callable, a callable that matches the method signature of
708 httplib2.Http.request, used to make the revoke request.
709 """
710 self._do_revoke(http_request, self.refresh_token)
711
712 def _do_revoke(self, http_request, token):
713 """Revokes the credentials and deletes the store if available.
714
715 Args:
716 http_request: callable, a callable that matches the method signature of
717 httplib2.Http.request, used to make the refresh request.
718 token: A string used as the token to be revoked. Can be either an
719 access_token or refresh_token.
720
721 Raises:
722 TokenRevokeError: If the revoke request does not return with a 200 OK.
723 """
724 logger.info('Revoking token')
725 query_params = {'token': token}
726 token_revoke_uri = _update_query_params(self.revoke_uri, query_params)
727 resp, content = http_request(token_revoke_uri)
728 if resp.status == 200:
729 self.invalid = True
730 else:
731 error_msg = 'Invalid response %s.' % resp.status
732 try:
733 d = simplejson.loads(content)
734 if 'error' in d:
735 error_msg = d['error']
736 except StandardError:
737 pass
738 raise TokenRevokeError(error_msg)
739
740 if self.store:
741 self.store.delete()
742
Joe Gregorio695fdc12011-01-16 16:46:55 -0500743
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500744class AccessTokenCredentials(OAuth2Credentials):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400745 """Credentials object for OAuth 2.0.
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500746
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400747 Credentials can be applied to an httplib2.Http object using the
748 authorize() method, which then signs each request from that object
Joe Gregorioe2233cd2013-01-24 15:46:23 -0500749 with the OAuth 2.0 access token. This set of credentials is for the
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400750 use case where you have acquired an OAuth 2.0 access_token from
751 another place such as a JavaScript client or another web
752 application, and wish to use it from Python. Because only the
753 access_token is present it can not be refreshed and will in time
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500754 expire.
755
Joe Gregorio9ce4b622011-02-17 15:32:11 -0500756 AccessTokenCredentials objects may be safely pickled and unpickled.
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500757
758 Usage:
759 credentials = AccessTokenCredentials('<an access token>',
760 'my-user-agent/1.0')
761 http = httplib2.Http()
762 http = credentials.authorize(http)
763
764 Exceptions:
765 AccessTokenCredentialsExpired: raised when the access_token expires or is
766 revoked.
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500767 """
768
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800769 def __init__(self, access_token, user_agent, revoke_uri=None):
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500770 """Create an instance of OAuth2Credentials
771
772 This is one of the few types if Credentials that you should contrust,
773 Credentials objects are usually instantiated by a Flow.
774
775 Args:
ade@google.com93a7f7c2011-02-23 16:00:37 +0000776 access_token: string, access token.
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500777 user_agent: string, The HTTP User-Agent to provide for this application.
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800778 revoke_uri: string, URI for revoke endpoint. Defaults to None; a token
779 can't be revoked if this is None.
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500780 """
781 super(AccessTokenCredentials, self).__init__(
782 access_token,
783 None,
784 None,
785 None,
786 None,
787 None,
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800788 user_agent,
789 revoke_uri=revoke_uri)
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500790
Joe Gregorio562b7312011-09-15 09:06:38 -0400791
792 @classmethod
793 def from_json(cls, s):
794 data = simplejson.loads(s)
795 retval = AccessTokenCredentials(
796 data['access_token'],
797 data['user_agent'])
798 return retval
799
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500800 def _refresh(self, http_request):
801 raise AccessTokenCredentialsError(
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800802 'The access_token is expired or invalid and can\'t be refreshed.')
803
804 def _revoke(self, http_request):
805 """Revokes the access_token and deletes the store if available.
806
807 Args:
808 http_request: callable, a callable that matches the method signature of
809 httplib2.Http.request, used to make the revoke request.
810 """
811 self._do_revoke(http_request, self.access_token)
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500812
JacobMoshenko8e905102011-06-20 09:53:10 -0400813
814class AssertionCredentials(OAuth2Credentials):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400815 """Abstract Credentials object used for OAuth 2.0 assertion grants.
JacobMoshenko8e905102011-06-20 09:53:10 -0400816
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400817 This credential does not require a flow to instantiate because it
818 represents a two legged flow, and therefore has all of the required
Joe Gregorioe2233cd2013-01-24 15:46:23 -0500819 information to generate and refresh its own access tokens. It must
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400820 be subclassed to generate the appropriate assertion string.
JacobMoshenko8e905102011-06-20 09:53:10 -0400821
822 AssertionCredentials objects may be safely pickled and unpickled.
823 """
824
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400825 @util.positional(2)
826 def __init__(self, assertion_type, user_agent=None,
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800827 token_uri=GOOGLE_TOKEN_URI,
828 revoke_uri=GOOGLE_REVOKE_URI,
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400829 **unused_kwargs):
830 """Constructor for AssertionFlowCredentials.
JacobMoshenko8e905102011-06-20 09:53:10 -0400831
832 Args:
833 assertion_type: string, assertion type that will be declared to the auth
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800834 server
JacobMoshenko8e905102011-06-20 09:53:10 -0400835 user_agent: string, The HTTP User-Agent to provide for this application.
836 token_uri: string, URI for token endpoint. For convenience
837 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800838 revoke_uri: string, URI for revoke endpoint.
JacobMoshenko8e905102011-06-20 09:53:10 -0400839 """
840 super(AssertionCredentials, self).__init__(
841 None,
842 None,
843 None,
844 None,
845 None,
846 token_uri,
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800847 user_agent,
848 revoke_uri=revoke_uri)
JacobMoshenko8e905102011-06-20 09:53:10 -0400849 self.assertion_type = assertion_type
850
851 def _generate_refresh_request_body(self):
852 assertion = self._generate_assertion()
853
854 body = urllib.urlencode({
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400855 'assertion_type': self.assertion_type,
856 'assertion': assertion,
857 'grant_type': 'assertion',
858 })
JacobMoshenko8e905102011-06-20 09:53:10 -0400859
860 return body
861
862 def _generate_assertion(self):
863 """Generate the assertion string that will be used in the access token
864 request.
865 """
866 _abstract()
867
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800868 def _revoke(self, http_request):
869 """Revokes the access_token and deletes the store if available.
870
871 Args:
872 http_request: callable, a callable that matches the method signature of
873 httplib2.Http.request, used to make the revoke request.
874 """
875 self._do_revoke(http_request, self.access_token)
876
877
Joe Gregorio0b723c22013-01-03 15:00:50 -0500878if HAS_CRYPTO:
879 # PyOpenSSL and PyCrypto are not prerequisites for oauth2client, so if it is
880 # missing then don't create the SignedJwtAssertionCredentials or the
881 # verify_id_token() method.
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500882
883 class SignedJwtAssertionCredentials(AssertionCredentials):
884 """Credentials object used for OAuth 2.0 Signed JWT assertion grants.
885
Joe Gregorio672051e2012-07-10 09:11:45 -0400886 This credential does not require a flow to instantiate because it represents
887 a two legged flow, and therefore has all of the required information to
888 generate and refresh its own access tokens.
889
Joe Gregorio0b723c22013-01-03 15:00:50 -0500890 SignedJwtAssertionCredentials requires either PyOpenSSL, or PyCrypto 2.6 or
891 later. For App Engine you may also consider using AppAssertionCredentials.
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500892 """
893
894 MAX_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds
895
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400896 @util.positional(4)
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500897 def __init__(self,
898 service_account_name,
899 private_key,
900 scope,
901 private_key_password='notasecret',
902 user_agent=None,
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800903 token_uri=GOOGLE_TOKEN_URI,
904 revoke_uri=GOOGLE_REVOKE_URI,
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500905 **kwargs):
906 """Constructor for SignedJwtAssertionCredentials.
907
908 Args:
909 service_account_name: string, id for account, usually an email address.
Joe Gregorio0b723c22013-01-03 15:00:50 -0500910 private_key: string, private key in PKCS12 or PEM format.
Joe Gregorio5cf5d122012-11-16 16:36:12 -0500911 scope: string or iterable of strings, scope(s) of the credentials being
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500912 requested.
Joe Gregorio0b723c22013-01-03 15:00:50 -0500913 private_key_password: string, password for private_key, unused if
914 private_key is in PEM format.
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500915 user_agent: string, HTTP User-Agent to provide for this application.
916 token_uri: string, URI for token endpoint. For convenience
917 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800918 revoke_uri: string, URI for revoke endpoint.
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500919 kwargs: kwargs, Additional parameters to add to the JWT token, for
920 example prn=joe@xample.org."""
921
922 super(SignedJwtAssertionCredentials, self).__init__(
923 'http://oauth.net/grant_type/jwt/1.0/bearer',
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400924 user_agent=user_agent,
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500925 token_uri=token_uri,
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800926 revoke_uri=revoke_uri,
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500927 )
928
Joe Gregorio5cf5d122012-11-16 16:36:12 -0500929 self.scope = util.scopes_to_string(scope)
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500930
Joe Gregorioa19f3a72012-07-11 15:35:35 -0400931 # Keep base64 encoded so it can be stored in JSON.
932 self.private_key = base64.b64encode(private_key)
933
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500934 self.private_key_password = private_key_password
935 self.service_account_name = service_account_name
936 self.kwargs = kwargs
937
938 @classmethod
939 def from_json(cls, s):
940 data = simplejson.loads(s)
941 retval = SignedJwtAssertionCredentials(
942 data['service_account_name'],
Joe Gregorioa19f3a72012-07-11 15:35:35 -0400943 base64.b64decode(data['private_key']),
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500944 data['scope'],
Joe Gregorioa19f3a72012-07-11 15:35:35 -0400945 private_key_password=data['private_key_password'],
946 user_agent=data['user_agent'],
947 token_uri=data['token_uri'],
948 **data['kwargs']
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500949 )
950 retval.invalid = data['invalid']
Joe Gregorioa19f3a72012-07-11 15:35:35 -0400951 retval.access_token = data['access_token']
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500952 return retval
953
954 def _generate_assertion(self):
955 """Generate the assertion that will be used in the request."""
956 now = long(time.time())
957 payload = {
958 'aud': self.token_uri,
959 'scope': self.scope,
960 'iat': now,
961 'exp': now + SignedJwtAssertionCredentials.MAX_TOKEN_LIFETIME_SECS,
962 'iss': self.service_account_name
963 }
964 payload.update(self.kwargs)
Joe Gregorioe78621a2012-03-09 15:47:23 -0500965 logger.debug(str(payload))
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500966
Joe Gregorioa19f3a72012-07-11 15:35:35 -0400967 private_key = base64.b64decode(self.private_key)
Joe Gregorio0b723c22013-01-03 15:00:50 -0500968 return crypt.make_signed_jwt(crypt.Signer.from_string(
969 private_key, self.private_key_password), payload)
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500970
Joe Gregorio9f2f38f2012-02-06 12:53:00 -0500971 # Only used in verify_id_token(), which is always calling to the same URI
972 # for the certs.
973 _cached_http = httplib2.Http(MemoryCache())
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500974
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400975 @util.positional(2)
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500976 def verify_id_token(id_token, audience, http=None,
977 cert_uri=ID_TOKEN_VERIFICATON_CERTS):
978 """Verifies a signed JWT id_token.
979
Joe Gregorio672051e2012-07-10 09:11:45 -0400980 This function requires PyOpenSSL and because of that it does not work on
Joe Gregorio0b723c22013-01-03 15:00:50 -0500981 App Engine.
Joe Gregorio672051e2012-07-10 09:11:45 -0400982
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500983 Args:
984 id_token: string, A Signed JWT.
985 audience: string, The audience 'aud' that the token should be for.
986 http: httplib2.Http, instance to use to make the HTTP request. Callers
987 should supply an instance that has caching enabled.
988 cert_uri: string, URI of the certificates in JSON format to
989 verify the JWT against.
990
991 Returns:
992 The deserialized JSON in the JWT.
993
994 Raises:
995 oauth2client.crypt.AppIdentityError if the JWT fails to verify.
996 """
997 if http is None:
Joe Gregorio9f2f38f2012-02-06 12:53:00 -0500998 http = _cached_http
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500999
1000 resp, content = http.request(cert_uri)
1001
1002 if resp.status == 200:
1003 certs = simplejson.loads(content)
Joe Gregorio0b723c22013-01-03 15:00:50 -05001004 return crypt.verify_signed_jwt_with_certs(id_token, certs, audience)
Joe Gregorio8b4c1732011-12-06 11:28:29 -05001005 else:
1006 raise VerifyJwtTokenError('Status code: %d' % resp.status)
1007
1008
1009def _urlsafe_b64decode(b64string):
Joe Gregoriobd512b52011-12-06 15:39:26 -05001010 # Guard against unicode strings, which base64 can't handle.
1011 b64string = b64string.encode('ascii')
Joe Gregorio8b4c1732011-12-06 11:28:29 -05001012 padded = b64string + '=' * (4 - len(b64string) % 4)
1013 return base64.urlsafe_b64decode(padded)
1014
1015
1016def _extract_id_token(id_token):
1017 """Extract the JSON payload from a JWT.
1018
1019 Does the extraction w/o checking the signature.
1020
1021 Args:
1022 id_token: string, OAuth 2.0 id_token.
1023
1024 Returns:
1025 object, The deserialized JSON payload.
1026 """
1027 segments = id_token.split('.')
1028
1029 if (len(segments) != 3):
1030 raise VerifyJwtTokenError(
1031 'Wrong number of segments in token: %s' % id_token)
1032
1033 return simplejson.loads(_urlsafe_b64decode(segments[1]))
1034
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001035
Joe Gregorioddb969a2012-07-11 11:04:12 -04001036def _parse_exchange_token_response(content):
1037 """Parses response of an exchange token request.
1038
1039 Most providers return JSON but some (e.g. Facebook) return a
1040 url-encoded string.
1041
1042 Args:
1043 content: The body of a response
1044
1045 Returns:
1046 Content as a dictionary object. Note that the dict could be empty,
1047 i.e. {}. That basically indicates a failure.
1048 """
1049 resp = {}
1050 try:
1051 resp = simplejson.loads(content)
1052 except StandardError:
1053 # different JSON libs raise different exceptions,
1054 # so we just do a catch-all here
1055 resp = dict(parse_qsl(content))
1056
1057 # some providers respond with 'expires', others with 'expires_in'
1058 if resp and 'expires' in resp:
1059 resp['expires_in'] = resp.pop('expires')
1060
1061 return resp
1062
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001063
1064@util.positional(4)
Joe Gregorio32d852d2012-06-14 09:08:18 -04001065def credentials_from_code(client_id, client_secret, scope, code,
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -08001066 redirect_uri='postmessage', http=None,
1067 user_agent=None, token_uri=GOOGLE_TOKEN_URI,
1068 auth_uri=GOOGLE_AUTH_URI,
1069 revoke_uri=GOOGLE_REVOKE_URI):
Joe Gregorio32d852d2012-06-14 09:08:18 -04001070 """Exchanges an authorization code for an OAuth2Credentials object.
1071
1072 Args:
1073 client_id: string, client identifier.
1074 client_secret: string, client secret.
Joe Gregorio5cf5d122012-11-16 16:36:12 -05001075 scope: string or iterable of strings, scope(s) to request.
Joe Gregorio32d852d2012-06-14 09:08:18 -04001076 code: string, An authroization code, most likely passed down from
1077 the client
1078 redirect_uri: string, this is generally set to 'postmessage' to match the
1079 redirect_uri that the client specified
1080 http: httplib2.Http, optional http instance to use to do the fetch
1081 token_uri: string, URI for token endpoint. For convenience
1082 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -08001083 auth_uri: string, URI for authorization endpoint. For convenience
1084 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
1085 revoke_uri: string, URI for revoke endpoint. For convenience
1086 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
1087
Joe Gregorio32d852d2012-06-14 09:08:18 -04001088 Returns:
1089 An OAuth2Credentials object.
1090
1091 Raises:
1092 FlowExchangeError if the authorization code cannot be exchanged for an
1093 access token
1094 """
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001095 flow = OAuth2WebServerFlow(client_id, client_secret, scope,
1096 redirect_uri=redirect_uri, user_agent=user_agent,
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -08001097 auth_uri=auth_uri, token_uri=token_uri,
1098 revoke_uri=revoke_uri)
Joe Gregorio32d852d2012-06-14 09:08:18 -04001099
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001100 credentials = flow.step2_exchange(code, http=http)
Joe Gregorio32d852d2012-06-14 09:08:18 -04001101 return credentials
1102
1103
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001104@util.positional(3)
Joe Gregorio32d852d2012-06-14 09:08:18 -04001105def credentials_from_clientsecrets_and_code(filename, scope, code,
1106 message = None,
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001107 redirect_uri='postmessage',
Joe Gregorioc29aaa92012-07-16 16:16:31 -04001108 http=None,
1109 cache=None):
Joe Gregorio32d852d2012-06-14 09:08:18 -04001110 """Returns OAuth2Credentials from a clientsecrets file and an auth code.
1111
1112 Will create the right kind of Flow based on the contents of the clientsecrets
1113 file or will raise InvalidClientSecretsError for unknown types of Flows.
1114
1115 Args:
1116 filename: string, File name of clientsecrets.
Joe Gregorio5cf5d122012-11-16 16:36:12 -05001117 scope: string or iterable of strings, scope(s) to request.
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001118 code: string, An authorization code, most likely passed down from
Joe Gregorio32d852d2012-06-14 09:08:18 -04001119 the client
1120 message: string, A friendly string to display to the user if the
1121 clientsecrets file is missing or invalid. If message is provided then
1122 sys.exit will be called in the case of an error. If message in not
1123 provided then clientsecrets.InvalidClientSecretsError will be raised.
1124 redirect_uri: string, this is generally set to 'postmessage' to match the
1125 redirect_uri that the client specified
1126 http: httplib2.Http, optional http instance to use to do the fetch
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001127 cache: An optional cache service client that implements get() and set()
Joe Gregorioc29aaa92012-07-16 16:16:31 -04001128 methods. See clientsecrets.loadfile() for details.
Joe Gregorio32d852d2012-06-14 09:08:18 -04001129
1130 Returns:
1131 An OAuth2Credentials object.
1132
1133 Raises:
1134 FlowExchangeError if the authorization code cannot be exchanged for an
1135 access token
1136 UnknownClientSecretsFlowError if the file describes an unknown kind of Flow.
1137 clientsecrets.InvalidClientSecretsError if the clientsecrets file is
1138 invalid.
1139 """
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001140 flow = flow_from_clientsecrets(filename, scope, message=message, cache=cache,
1141 redirect_uri=redirect_uri)
1142 credentials = flow.step2_exchange(code, http=http)
Joe Gregorio32d852d2012-06-14 09:08:18 -04001143 return credentials
1144
JacobMoshenko8e905102011-06-20 09:53:10 -04001145
Joe Gregorio695fdc12011-01-16 16:46:55 -05001146class OAuth2WebServerFlow(Flow):
1147 """Does the Web Server Flow for OAuth 2.0.
1148
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001149 OAuth2WebServerFlow objects may be safely pickled and unpickled.
Joe Gregorio695fdc12011-01-16 16:46:55 -05001150 """
1151
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001152 @util.positional(4)
1153 def __init__(self, client_id, client_secret, scope,
1154 redirect_uri=None,
1155 user_agent=None,
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -08001156 auth_uri=GOOGLE_AUTH_URI,
1157 token_uri=GOOGLE_TOKEN_URI,
1158 revoke_uri=GOOGLE_REVOKE_URI,
Joe Gregorio9da2ad82011-09-11 14:04:44 -04001159 **kwargs):
1160 """Constructor for OAuth2WebServerFlow.
Joe Gregorio695fdc12011-01-16 16:46:55 -05001161
Joe Gregorio32f73192012-10-23 16:13:44 -04001162 The kwargs argument is used to set extra query parameters on the
1163 auth_uri. For example, the access_type and approval_prompt
1164 query parameters can be set via kwargs.
1165
Joe Gregorio695fdc12011-01-16 16:46:55 -05001166 Args:
Joe Gregorio7c22ab22011-02-16 15:32:39 -05001167 client_id: string, client identifier.
1168 client_secret: string client secret.
Joe Gregorio5cf5d122012-11-16 16:36:12 -05001169 scope: string or iterable of strings, scope(s) of the credentials being
Joe Gregoriof2f8a5a2011-10-14 15:11:29 -04001170 requested.
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001171 redirect_uri: string, Either the string 'urn:ietf:wg:oauth:2.0:oob' for
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -08001172 a non-web-based application, or a URI that handles the callback from
1173 the authorization server.
Joe Gregorio695fdc12011-01-16 16:46:55 -05001174 user_agent: string, HTTP User-Agent to provide for this application.
Joe Gregorio7c22ab22011-02-16 15:32:39 -05001175 auth_uri: string, URI for authorization endpoint. For convenience
1176 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
1177 token_uri: string, URI for token endpoint. For convenience
1178 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -08001179 revoke_uri: string, URI for revoke endpoint. For convenience
1180 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
Joe Gregorio695fdc12011-01-16 16:46:55 -05001181 **kwargs: dict, The keyword arguments are all optional and required
1182 parameters for the OAuth calls.
1183 """
1184 self.client_id = client_id
1185 self.client_secret = client_secret
Joe Gregorio5cf5d122012-11-16 16:36:12 -05001186 self.scope = util.scopes_to_string(scope)
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001187 self.redirect_uri = redirect_uri
Joe Gregorio695fdc12011-01-16 16:46:55 -05001188 self.user_agent = user_agent
Joe Gregorio7c22ab22011-02-16 15:32:39 -05001189 self.auth_uri = auth_uri
Joe Gregorio695fdc12011-01-16 16:46:55 -05001190 self.token_uri = token_uri
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -08001191 self.revoke_uri = revoke_uri
Joe Gregorio69a0aca2011-11-03 10:47:32 -04001192 self.params = {
1193 'access_type': 'offline',
Joe Gregorio32f73192012-10-23 16:13:44 -04001194 'response_type': 'code',
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -08001195 }
Joe Gregorio69a0aca2011-11-03 10:47:32 -04001196 self.params.update(kwargs)
Joe Gregorio695fdc12011-01-16 16:46:55 -05001197
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001198 @util.positional(1)
1199 def step1_get_authorize_url(self, redirect_uri=None):
Joe Gregorio695fdc12011-01-16 16:46:55 -05001200 """Returns a URI to redirect to the provider.
1201
1202 Args:
Joe Gregoriof2326c02012-02-09 12:18:44 -05001203 redirect_uri: string, Either the string 'urn:ietf:wg:oauth:2.0:oob' for
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -08001204 a non-web-based application, or a URI that handles the callback from
1205 the authorization server. This parameter is deprecated, please move to
1206 passing the redirect_uri in via the constructor.
Joe Gregorio695fdc12011-01-16 16:46:55 -05001207
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001208 Returns:
1209 A URI as a string to redirect the user to begin the authorization flow.
Joe Gregorio695fdc12011-01-16 16:46:55 -05001210 """
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001211 if redirect_uri is not None:
1212 logger.warning(('The redirect_uri parameter for'
1213 'OAuth2WebServerFlow.step1_get_authorize_url is deprecated. Please'
1214 'move to passing the redirect_uri in via the constructor.'))
1215 self.redirect_uri = redirect_uri
Joe Gregorio695fdc12011-01-16 16:46:55 -05001216
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001217 if self.redirect_uri is None:
1218 raise ValueError('The value of redirect_uri must not be None.')
1219
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -08001220 query_params = {
Joe Gregorio9da2ad82011-09-11 14:04:44 -04001221 'client_id': self.client_id,
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001222 'redirect_uri': self.redirect_uri,
Joe Gregorio9da2ad82011-09-11 14:04:44 -04001223 'scope': self.scope,
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -08001224 }
1225 query_params.update(self.params)
1226 return _update_query_params(self.auth_uri, query_params)
Joe Gregorio695fdc12011-01-16 16:46:55 -05001227
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001228 @util.positional(2)
Joe Gregorioccc79542011-02-19 00:05:26 -05001229 def step2_exchange(self, code, http=None):
Joe Gregorio695fdc12011-01-16 16:46:55 -05001230 """Exhanges a code for OAuth2Credentials.
1231
1232 Args:
1233 code: string or dict, either the code as a string, or a dictionary
1234 of the query parameters to the redirect_uri, which contains
1235 the code.
Joe Gregorioccc79542011-02-19 00:05:26 -05001236 http: httplib2.Http, optional http instance to use to do the fetch
Joe Gregorio4b4002f2012-06-14 15:41:01 -04001237
1238 Returns:
1239 An OAuth2Credentials object that can be used to authorize requests.
1240
1241 Raises:
1242 FlowExchangeError if a problem occured exchanging the code for a
1243 refresh_token.
Joe Gregorio695fdc12011-01-16 16:46:55 -05001244 """
1245
1246 if not (isinstance(code, str) or isinstance(code, unicode)):
Joe Gregorio4b4002f2012-06-14 15:41:01 -04001247 if 'code' not in code:
1248 if 'error' in code:
1249 error_msg = code['error']
1250 else:
1251 error_msg = 'No code was supplied in the query parameters.'
1252 raise FlowExchangeError(error_msg)
1253 else:
1254 code = code['code']
Joe Gregorio695fdc12011-01-16 16:46:55 -05001255
1256 body = urllib.urlencode({
Joe Gregorio9da2ad82011-09-11 14:04:44 -04001257 'grant_type': 'authorization_code',
1258 'client_id': self.client_id,
1259 'client_secret': self.client_secret,
1260 'code': code,
1261 'redirect_uri': self.redirect_uri,
1262 'scope': self.scope,
1263 })
Joe Gregorio695fdc12011-01-16 16:46:55 -05001264 headers = {
Joe Gregorio9da2ad82011-09-11 14:04:44 -04001265 'content-type': 'application/x-www-form-urlencoded',
Joe Gregorio695fdc12011-01-16 16:46:55 -05001266 }
JacobMoshenkocb6d8912011-07-08 13:35:15 -04001267
1268 if self.user_agent is not None:
1269 headers['user-agent'] = self.user_agent
1270
Joe Gregorioccc79542011-02-19 00:05:26 -05001271 if http is None:
1272 http = httplib2.Http()
Joe Gregorio8b4c1732011-12-06 11:28:29 -05001273
JacobMoshenko8e905102011-06-20 09:53:10 -04001274 resp, content = http.request(self.token_uri, method='POST', body=body,
1275 headers=headers)
Joe Gregorioddb969a2012-07-11 11:04:12 -04001276 d = _parse_exchange_token_response(content)
1277 if resp.status == 200 and 'access_token' in d:
Joe Gregorio695fdc12011-01-16 16:46:55 -05001278 access_token = d['access_token']
1279 refresh_token = d.get('refresh_token', None)
1280 token_expiry = None
1281 if 'expires_in' in d:
Joe Gregorio562b7312011-09-15 09:06:38 -04001282 token_expiry = datetime.datetime.utcnow() + datetime.timedelta(
JacobMoshenko8e905102011-06-20 09:53:10 -04001283 seconds=int(d['expires_in']))
Joe Gregorio695fdc12011-01-16 16:46:55 -05001284
Joe Gregorio8b4c1732011-12-06 11:28:29 -05001285 if 'id_token' in d:
1286 d['id_token'] = _extract_id_token(d['id_token'])
1287
Joe Gregorio6ceea2d2012-08-24 11:57:58 -04001288 logger.info('Successfully retrieved access token')
JacobMoshenko8e905102011-06-20 09:53:10 -04001289 return OAuth2Credentials(access_token, self.client_id,
1290 self.client_secret, refresh_token, token_expiry,
Joe Gregorio8b4c1732011-12-06 11:28:29 -05001291 self.token_uri, self.user_agent,
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -08001292 revoke_uri=self.revoke_uri,
Joe Gregorio8b4c1732011-12-06 11:28:29 -05001293 id_token=d.get('id_token', None))
Joe Gregorio695fdc12011-01-16 16:46:55 -05001294 else:
Joe Gregorioe78621a2012-03-09 15:47:23 -05001295 logger.info('Failed to retrieve access token: %s' % content)
Joe Gregorioddb969a2012-07-11 11:04:12 -04001296 if 'error' in d:
1297 # you never know what those providers got to say
1298 error_msg = unicode(d['error'])
1299 else:
1300 error_msg = 'Invalid response: %s.' % str(resp.status)
Joe Gregorioccc79542011-02-19 00:05:26 -05001301 raise FlowExchangeError(error_msg)
Joe Gregoriof08a4982011-10-07 13:11:16 -04001302
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001303
1304@util.positional(2)
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -08001305def flow_from_clientsecrets(filename, scope, redirect_uri=None,
1306 message=None, cache=None):
Joe Gregoriof08a4982011-10-07 13:11:16 -04001307 """Create a Flow from a clientsecrets file.
1308
1309 Will create the right kind of Flow based on the contents of the clientsecrets
1310 file or will raise InvalidClientSecretsError for unknown types of Flows.
1311
1312 Args:
1313 filename: string, File name of client secrets.
Joe Gregorio5cf5d122012-11-16 16:36:12 -05001314 scope: string or iterable of strings, scope(s) to request.
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001315 redirect_uri: string, Either the string 'urn:ietf:wg:oauth:2.0:oob' for
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -08001316 a non-web-based application, or a URI that handles the callback from
1317 the authorization server.
Joe Gregoriof08a4982011-10-07 13:11:16 -04001318 message: string, A friendly string to display to the user if the
1319 clientsecrets file is missing or invalid. If message is provided then
1320 sys.exit will be called in the case of an error. If message in not
1321 provided then clientsecrets.InvalidClientSecretsError will be raised.
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001322 cache: An optional cache service client that implements get() and set()
Joe Gregorioc29aaa92012-07-16 16:16:31 -04001323 methods. See clientsecrets.loadfile() for details.
Joe Gregoriof08a4982011-10-07 13:11:16 -04001324
1325 Returns:
1326 A Flow object.
1327
1328 Raises:
1329 UnknownClientSecretsFlowError if the file describes an unknown kind of Flow.
1330 clientsecrets.InvalidClientSecretsError if the clientsecrets file is
1331 invalid.
1332 """
Joe Gregorio0984ef22011-10-14 13:17:43 -04001333 try:
Joe Gregorioc29aaa92012-07-16 16:16:31 -04001334 client_type, client_info = clientsecrets.loadfile(filename, cache=cache)
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -08001335 if client_type in (clientsecrets.TYPE_WEB, clientsecrets.TYPE_INSTALLED):
1336 constructor_kwargs = {
1337 'redirect_uri': redirect_uri,
1338 'auth_uri': client_info['auth_uri'],
1339 'token_uri': client_info['token_uri'],
1340 }
1341 revoke_uri = client_info.get('revoke_uri')
1342 if revoke_uri is not None:
1343 constructor_kwargs['revoke_uri'] = revoke_uri
1344 return OAuth2WebServerFlow(
1345 client_info['client_id'], client_info['client_secret'],
1346 scope, **constructor_kwargs)
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001347
Joe Gregorio0984ef22011-10-14 13:17:43 -04001348 except clientsecrets.InvalidClientSecretsError:
1349 if message:
1350 sys.exit(message)
1351 else:
1352 raise
Joe Gregoriof08a4982011-10-07 13:11:16 -04001353 else:
1354 raise UnknownClientSecretsFlowError(
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -08001355 'This OAuth 2.0 flow is unsupported: %r' % client_type)