blob: 3370688bb11185a61255fee1d274250ea5f6a0a1 [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': assertion,
Joe Gregoriocdc350f2013-02-07 10:52:26 -0500856 'grant_type': 'urn:ietf:params:oauth:grant-type:jwt-bearer',
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400857 })
JacobMoshenko8e905102011-06-20 09:53:10 -0400858
859 return body
860
861 def _generate_assertion(self):
862 """Generate the assertion string that will be used in the access token
863 request.
864 """
865 _abstract()
866
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800867 def _revoke(self, http_request):
868 """Revokes the access_token and deletes the store if available.
869
870 Args:
871 http_request: callable, a callable that matches the method signature of
872 httplib2.Http.request, used to make the revoke request.
873 """
874 self._do_revoke(http_request, self.access_token)
875
876
Joe Gregorio0b723c22013-01-03 15:00:50 -0500877if HAS_CRYPTO:
878 # PyOpenSSL and PyCrypto are not prerequisites for oauth2client, so if it is
879 # missing then don't create the SignedJwtAssertionCredentials or the
880 # verify_id_token() method.
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500881
882 class SignedJwtAssertionCredentials(AssertionCredentials):
883 """Credentials object used for OAuth 2.0 Signed JWT assertion grants.
884
Joe Gregorio672051e2012-07-10 09:11:45 -0400885 This credential does not require a flow to instantiate because it represents
886 a two legged flow, and therefore has all of the required information to
887 generate and refresh its own access tokens.
888
Joe Gregorio0b723c22013-01-03 15:00:50 -0500889 SignedJwtAssertionCredentials requires either PyOpenSSL, or PyCrypto 2.6 or
890 later. For App Engine you may also consider using AppAssertionCredentials.
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500891 """
892
893 MAX_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds
894
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400895 @util.positional(4)
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500896 def __init__(self,
897 service_account_name,
898 private_key,
899 scope,
900 private_key_password='notasecret',
901 user_agent=None,
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800902 token_uri=GOOGLE_TOKEN_URI,
903 revoke_uri=GOOGLE_REVOKE_URI,
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500904 **kwargs):
905 """Constructor for SignedJwtAssertionCredentials.
906
907 Args:
908 service_account_name: string, id for account, usually an email address.
Joe Gregorio0b723c22013-01-03 15:00:50 -0500909 private_key: string, private key in PKCS12 or PEM format.
Joe Gregorio5cf5d122012-11-16 16:36:12 -0500910 scope: string or iterable of strings, scope(s) of the credentials being
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500911 requested.
Joe Gregorio0b723c22013-01-03 15:00:50 -0500912 private_key_password: string, password for private_key, unused if
913 private_key is in PEM format.
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500914 user_agent: string, HTTP User-Agent to provide for this application.
915 token_uri: string, URI for token endpoint. For convenience
916 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800917 revoke_uri: string, URI for revoke endpoint.
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500918 kwargs: kwargs, Additional parameters to add to the JWT token, for
919 example prn=joe@xample.org."""
920
921 super(SignedJwtAssertionCredentials, self).__init__(
dhermes@google.com2cc09382013-02-11 08:42:18 -0800922 None,
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400923 user_agent=user_agent,
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500924 token_uri=token_uri,
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800925 revoke_uri=revoke_uri,
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500926 )
927
Joe Gregorio5cf5d122012-11-16 16:36:12 -0500928 self.scope = util.scopes_to_string(scope)
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500929
Joe Gregorioa19f3a72012-07-11 15:35:35 -0400930 # Keep base64 encoded so it can be stored in JSON.
931 self.private_key = base64.b64encode(private_key)
932
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500933 self.private_key_password = private_key_password
934 self.service_account_name = service_account_name
935 self.kwargs = kwargs
936
937 @classmethod
938 def from_json(cls, s):
939 data = simplejson.loads(s)
940 retval = SignedJwtAssertionCredentials(
941 data['service_account_name'],
Joe Gregorioa19f3a72012-07-11 15:35:35 -0400942 base64.b64decode(data['private_key']),
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500943 data['scope'],
Joe Gregorioa19f3a72012-07-11 15:35:35 -0400944 private_key_password=data['private_key_password'],
945 user_agent=data['user_agent'],
946 token_uri=data['token_uri'],
947 **data['kwargs']
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500948 )
949 retval.invalid = data['invalid']
Joe Gregorioa19f3a72012-07-11 15:35:35 -0400950 retval.access_token = data['access_token']
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500951 return retval
952
953 def _generate_assertion(self):
954 """Generate the assertion that will be used in the request."""
955 now = long(time.time())
956 payload = {
957 'aud': self.token_uri,
958 'scope': self.scope,
959 'iat': now,
960 'exp': now + SignedJwtAssertionCredentials.MAX_TOKEN_LIFETIME_SECS,
961 'iss': self.service_account_name
962 }
963 payload.update(self.kwargs)
Joe Gregorioe78621a2012-03-09 15:47:23 -0500964 logger.debug(str(payload))
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500965
Joe Gregorioa19f3a72012-07-11 15:35:35 -0400966 private_key = base64.b64decode(self.private_key)
Joe Gregorio0b723c22013-01-03 15:00:50 -0500967 return crypt.make_signed_jwt(crypt.Signer.from_string(
968 private_key, self.private_key_password), payload)
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500969
Joe Gregorio9f2f38f2012-02-06 12:53:00 -0500970 # Only used in verify_id_token(), which is always calling to the same URI
971 # for the certs.
972 _cached_http = httplib2.Http(MemoryCache())
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500973
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400974 @util.positional(2)
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500975 def verify_id_token(id_token, audience, http=None,
976 cert_uri=ID_TOKEN_VERIFICATON_CERTS):
977 """Verifies a signed JWT id_token.
978
Joe Gregorio672051e2012-07-10 09:11:45 -0400979 This function requires PyOpenSSL and because of that it does not work on
Joe Gregorio0b723c22013-01-03 15:00:50 -0500980 App Engine.
Joe Gregorio672051e2012-07-10 09:11:45 -0400981
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500982 Args:
983 id_token: string, A Signed JWT.
984 audience: string, The audience 'aud' that the token should be for.
985 http: httplib2.Http, instance to use to make the HTTP request. Callers
986 should supply an instance that has caching enabled.
987 cert_uri: string, URI of the certificates in JSON format to
988 verify the JWT against.
989
990 Returns:
991 The deserialized JSON in the JWT.
992
993 Raises:
994 oauth2client.crypt.AppIdentityError if the JWT fails to verify.
995 """
996 if http is None:
Joe Gregorio9f2f38f2012-02-06 12:53:00 -0500997 http = _cached_http
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500998
999 resp, content = http.request(cert_uri)
1000
1001 if resp.status == 200:
1002 certs = simplejson.loads(content)
Joe Gregorio0b723c22013-01-03 15:00:50 -05001003 return crypt.verify_signed_jwt_with_certs(id_token, certs, audience)
Joe Gregorio8b4c1732011-12-06 11:28:29 -05001004 else:
1005 raise VerifyJwtTokenError('Status code: %d' % resp.status)
1006
1007
1008def _urlsafe_b64decode(b64string):
Joe Gregoriobd512b52011-12-06 15:39:26 -05001009 # Guard against unicode strings, which base64 can't handle.
1010 b64string = b64string.encode('ascii')
Joe Gregorio8b4c1732011-12-06 11:28:29 -05001011 padded = b64string + '=' * (4 - len(b64string) % 4)
1012 return base64.urlsafe_b64decode(padded)
1013
1014
1015def _extract_id_token(id_token):
1016 """Extract the JSON payload from a JWT.
1017
1018 Does the extraction w/o checking the signature.
1019
1020 Args:
1021 id_token: string, OAuth 2.0 id_token.
1022
1023 Returns:
1024 object, The deserialized JSON payload.
1025 """
1026 segments = id_token.split('.')
1027
1028 if (len(segments) != 3):
1029 raise VerifyJwtTokenError(
1030 'Wrong number of segments in token: %s' % id_token)
1031
1032 return simplejson.loads(_urlsafe_b64decode(segments[1]))
1033
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001034
Joe Gregorioddb969a2012-07-11 11:04:12 -04001035def _parse_exchange_token_response(content):
1036 """Parses response of an exchange token request.
1037
1038 Most providers return JSON but some (e.g. Facebook) return a
1039 url-encoded string.
1040
1041 Args:
1042 content: The body of a response
1043
1044 Returns:
1045 Content as a dictionary object. Note that the dict could be empty,
1046 i.e. {}. That basically indicates a failure.
1047 """
1048 resp = {}
1049 try:
1050 resp = simplejson.loads(content)
1051 except StandardError:
1052 # different JSON libs raise different exceptions,
1053 # so we just do a catch-all here
1054 resp = dict(parse_qsl(content))
1055
1056 # some providers respond with 'expires', others with 'expires_in'
1057 if resp and 'expires' in resp:
1058 resp['expires_in'] = resp.pop('expires')
1059
1060 return resp
1061
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001062
1063@util.positional(4)
Joe Gregorio32d852d2012-06-14 09:08:18 -04001064def credentials_from_code(client_id, client_secret, scope, code,
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -08001065 redirect_uri='postmessage', http=None,
1066 user_agent=None, token_uri=GOOGLE_TOKEN_URI,
1067 auth_uri=GOOGLE_AUTH_URI,
1068 revoke_uri=GOOGLE_REVOKE_URI):
Joe Gregorio32d852d2012-06-14 09:08:18 -04001069 """Exchanges an authorization code for an OAuth2Credentials object.
1070
1071 Args:
1072 client_id: string, client identifier.
1073 client_secret: string, client secret.
Joe Gregorio5cf5d122012-11-16 16:36:12 -05001074 scope: string or iterable of strings, scope(s) to request.
Joe Gregorio32d852d2012-06-14 09:08:18 -04001075 code: string, An authroization code, most likely passed down from
1076 the client
1077 redirect_uri: string, this is generally set to 'postmessage' to match the
1078 redirect_uri that the client specified
1079 http: httplib2.Http, optional http instance to use to do the fetch
1080 token_uri: string, URI for token endpoint. For convenience
1081 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -08001082 auth_uri: string, URI for authorization endpoint. For convenience
1083 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
1084 revoke_uri: string, URI for revoke endpoint. For convenience
1085 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
1086
Joe Gregorio32d852d2012-06-14 09:08:18 -04001087 Returns:
1088 An OAuth2Credentials object.
1089
1090 Raises:
1091 FlowExchangeError if the authorization code cannot be exchanged for an
1092 access token
1093 """
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001094 flow = OAuth2WebServerFlow(client_id, client_secret, scope,
1095 redirect_uri=redirect_uri, user_agent=user_agent,
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -08001096 auth_uri=auth_uri, token_uri=token_uri,
1097 revoke_uri=revoke_uri)
Joe Gregorio32d852d2012-06-14 09:08:18 -04001098
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001099 credentials = flow.step2_exchange(code, http=http)
Joe Gregorio32d852d2012-06-14 09:08:18 -04001100 return credentials
1101
1102
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001103@util.positional(3)
Joe Gregorio32d852d2012-06-14 09:08:18 -04001104def credentials_from_clientsecrets_and_code(filename, scope, code,
1105 message = None,
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001106 redirect_uri='postmessage',
Joe Gregorioc29aaa92012-07-16 16:16:31 -04001107 http=None,
1108 cache=None):
Joe Gregorio32d852d2012-06-14 09:08:18 -04001109 """Returns OAuth2Credentials from a clientsecrets file and an auth code.
1110
1111 Will create the right kind of Flow based on the contents of the clientsecrets
1112 file or will raise InvalidClientSecretsError for unknown types of Flows.
1113
1114 Args:
1115 filename: string, File name of clientsecrets.
Joe Gregorio5cf5d122012-11-16 16:36:12 -05001116 scope: string or iterable of strings, scope(s) to request.
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001117 code: string, An authorization code, most likely passed down from
Joe Gregorio32d852d2012-06-14 09:08:18 -04001118 the client
1119 message: string, A friendly string to display to the user if the
1120 clientsecrets file is missing or invalid. If message is provided then
1121 sys.exit will be called in the case of an error. If message in not
1122 provided then clientsecrets.InvalidClientSecretsError will be raised.
1123 redirect_uri: string, this is generally set to 'postmessage' to match the
1124 redirect_uri that the client specified
1125 http: httplib2.Http, optional http instance to use to do the fetch
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001126 cache: An optional cache service client that implements get() and set()
Joe Gregorioc29aaa92012-07-16 16:16:31 -04001127 methods. See clientsecrets.loadfile() for details.
Joe Gregorio32d852d2012-06-14 09:08:18 -04001128
1129 Returns:
1130 An OAuth2Credentials object.
1131
1132 Raises:
1133 FlowExchangeError if the authorization code cannot be exchanged for an
1134 access token
1135 UnknownClientSecretsFlowError if the file describes an unknown kind of Flow.
1136 clientsecrets.InvalidClientSecretsError if the clientsecrets file is
1137 invalid.
1138 """
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001139 flow = flow_from_clientsecrets(filename, scope, message=message, cache=cache,
1140 redirect_uri=redirect_uri)
1141 credentials = flow.step2_exchange(code, http=http)
Joe Gregorio32d852d2012-06-14 09:08:18 -04001142 return credentials
1143
JacobMoshenko8e905102011-06-20 09:53:10 -04001144
Joe Gregorio695fdc12011-01-16 16:46:55 -05001145class OAuth2WebServerFlow(Flow):
1146 """Does the Web Server Flow for OAuth 2.0.
1147
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001148 OAuth2WebServerFlow objects may be safely pickled and unpickled.
Joe Gregorio695fdc12011-01-16 16:46:55 -05001149 """
1150
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001151 @util.positional(4)
1152 def __init__(self, client_id, client_secret, scope,
1153 redirect_uri=None,
1154 user_agent=None,
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -08001155 auth_uri=GOOGLE_AUTH_URI,
1156 token_uri=GOOGLE_TOKEN_URI,
1157 revoke_uri=GOOGLE_REVOKE_URI,
Joe Gregorio9da2ad82011-09-11 14:04:44 -04001158 **kwargs):
1159 """Constructor for OAuth2WebServerFlow.
Joe Gregorio695fdc12011-01-16 16:46:55 -05001160
Joe Gregorio32f73192012-10-23 16:13:44 -04001161 The kwargs argument is used to set extra query parameters on the
1162 auth_uri. For example, the access_type and approval_prompt
1163 query parameters can be set via kwargs.
1164
Joe Gregorio695fdc12011-01-16 16:46:55 -05001165 Args:
Joe Gregorio7c22ab22011-02-16 15:32:39 -05001166 client_id: string, client identifier.
1167 client_secret: string client secret.
Joe Gregorio5cf5d122012-11-16 16:36:12 -05001168 scope: string or iterable of strings, scope(s) of the credentials being
Joe Gregoriof2f8a5a2011-10-14 15:11:29 -04001169 requested.
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001170 redirect_uri: string, Either the string 'urn:ietf:wg:oauth:2.0:oob' for
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -08001171 a non-web-based application, or a URI that handles the callback from
1172 the authorization server.
Joe Gregorio695fdc12011-01-16 16:46:55 -05001173 user_agent: string, HTTP User-Agent to provide for this application.
Joe Gregorio7c22ab22011-02-16 15:32:39 -05001174 auth_uri: string, URI for authorization endpoint. For convenience
1175 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
1176 token_uri: string, URI for token endpoint. For convenience
1177 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -08001178 revoke_uri: string, URI for revoke endpoint. For convenience
1179 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
Joe Gregorio695fdc12011-01-16 16:46:55 -05001180 **kwargs: dict, The keyword arguments are all optional and required
1181 parameters for the OAuth calls.
1182 """
1183 self.client_id = client_id
1184 self.client_secret = client_secret
Joe Gregorio5cf5d122012-11-16 16:36:12 -05001185 self.scope = util.scopes_to_string(scope)
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001186 self.redirect_uri = redirect_uri
Joe Gregorio695fdc12011-01-16 16:46:55 -05001187 self.user_agent = user_agent
Joe Gregorio7c22ab22011-02-16 15:32:39 -05001188 self.auth_uri = auth_uri
Joe Gregorio695fdc12011-01-16 16:46:55 -05001189 self.token_uri = token_uri
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -08001190 self.revoke_uri = revoke_uri
Joe Gregorio69a0aca2011-11-03 10:47:32 -04001191 self.params = {
1192 'access_type': 'offline',
Joe Gregorio32f73192012-10-23 16:13:44 -04001193 'response_type': 'code',
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -08001194 }
Joe Gregorio69a0aca2011-11-03 10:47:32 -04001195 self.params.update(kwargs)
Joe Gregorio695fdc12011-01-16 16:46:55 -05001196
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001197 @util.positional(1)
1198 def step1_get_authorize_url(self, redirect_uri=None):
Joe Gregorio695fdc12011-01-16 16:46:55 -05001199 """Returns a URI to redirect to the provider.
1200
1201 Args:
Joe Gregoriof2326c02012-02-09 12:18:44 -05001202 redirect_uri: string, Either the string 'urn:ietf:wg:oauth:2.0:oob' for
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -08001203 a non-web-based application, or a URI that handles the callback from
1204 the authorization server. This parameter is deprecated, please move to
1205 passing the redirect_uri in via the constructor.
Joe Gregorio695fdc12011-01-16 16:46:55 -05001206
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001207 Returns:
1208 A URI as a string to redirect the user to begin the authorization flow.
Joe Gregorio695fdc12011-01-16 16:46:55 -05001209 """
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001210 if redirect_uri is not None:
1211 logger.warning(('The redirect_uri parameter for'
1212 'OAuth2WebServerFlow.step1_get_authorize_url is deprecated. Please'
1213 'move to passing the redirect_uri in via the constructor.'))
1214 self.redirect_uri = redirect_uri
Joe Gregorio695fdc12011-01-16 16:46:55 -05001215
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001216 if self.redirect_uri is None:
1217 raise ValueError('The value of redirect_uri must not be None.')
1218
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -08001219 query_params = {
Joe Gregorio9da2ad82011-09-11 14:04:44 -04001220 'client_id': self.client_id,
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001221 'redirect_uri': self.redirect_uri,
Joe Gregorio9da2ad82011-09-11 14:04:44 -04001222 'scope': self.scope,
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -08001223 }
1224 query_params.update(self.params)
1225 return _update_query_params(self.auth_uri, query_params)
Joe Gregorio695fdc12011-01-16 16:46:55 -05001226
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001227 @util.positional(2)
Joe Gregorioccc79542011-02-19 00:05:26 -05001228 def step2_exchange(self, code, http=None):
Joe Gregorio695fdc12011-01-16 16:46:55 -05001229 """Exhanges a code for OAuth2Credentials.
1230
1231 Args:
1232 code: string or dict, either the code as a string, or a dictionary
1233 of the query parameters to the redirect_uri, which contains
1234 the code.
Joe Gregorioccc79542011-02-19 00:05:26 -05001235 http: httplib2.Http, optional http instance to use to do the fetch
Joe Gregorio4b4002f2012-06-14 15:41:01 -04001236
1237 Returns:
1238 An OAuth2Credentials object that can be used to authorize requests.
1239
1240 Raises:
1241 FlowExchangeError if a problem occured exchanging the code for a
1242 refresh_token.
Joe Gregorio695fdc12011-01-16 16:46:55 -05001243 """
1244
1245 if not (isinstance(code, str) or isinstance(code, unicode)):
Joe Gregorio4b4002f2012-06-14 15:41:01 -04001246 if 'code' not in code:
1247 if 'error' in code:
1248 error_msg = code['error']
1249 else:
1250 error_msg = 'No code was supplied in the query parameters.'
1251 raise FlowExchangeError(error_msg)
1252 else:
1253 code = code['code']
Joe Gregorio695fdc12011-01-16 16:46:55 -05001254
1255 body = urllib.urlencode({
Joe Gregorio9da2ad82011-09-11 14:04:44 -04001256 'grant_type': 'authorization_code',
1257 'client_id': self.client_id,
1258 'client_secret': self.client_secret,
1259 'code': code,
1260 'redirect_uri': self.redirect_uri,
1261 'scope': self.scope,
1262 })
Joe Gregorio695fdc12011-01-16 16:46:55 -05001263 headers = {
Joe Gregorio9da2ad82011-09-11 14:04:44 -04001264 'content-type': 'application/x-www-form-urlencoded',
Joe Gregorio695fdc12011-01-16 16:46:55 -05001265 }
JacobMoshenkocb6d8912011-07-08 13:35:15 -04001266
1267 if self.user_agent is not None:
1268 headers['user-agent'] = self.user_agent
1269
Joe Gregorioccc79542011-02-19 00:05:26 -05001270 if http is None:
1271 http = httplib2.Http()
Joe Gregorio8b4c1732011-12-06 11:28:29 -05001272
JacobMoshenko8e905102011-06-20 09:53:10 -04001273 resp, content = http.request(self.token_uri, method='POST', body=body,
1274 headers=headers)
Joe Gregorioddb969a2012-07-11 11:04:12 -04001275 d = _parse_exchange_token_response(content)
1276 if resp.status == 200 and 'access_token' in d:
Joe Gregorio695fdc12011-01-16 16:46:55 -05001277 access_token = d['access_token']
1278 refresh_token = d.get('refresh_token', None)
1279 token_expiry = None
1280 if 'expires_in' in d:
Joe Gregorio562b7312011-09-15 09:06:38 -04001281 token_expiry = datetime.datetime.utcnow() + datetime.timedelta(
JacobMoshenko8e905102011-06-20 09:53:10 -04001282 seconds=int(d['expires_in']))
Joe Gregorio695fdc12011-01-16 16:46:55 -05001283
Joe Gregorio8b4c1732011-12-06 11:28:29 -05001284 if 'id_token' in d:
1285 d['id_token'] = _extract_id_token(d['id_token'])
1286
Joe Gregorio6ceea2d2012-08-24 11:57:58 -04001287 logger.info('Successfully retrieved access token')
JacobMoshenko8e905102011-06-20 09:53:10 -04001288 return OAuth2Credentials(access_token, self.client_id,
1289 self.client_secret, refresh_token, token_expiry,
Joe Gregorio8b4c1732011-12-06 11:28:29 -05001290 self.token_uri, self.user_agent,
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -08001291 revoke_uri=self.revoke_uri,
Joe Gregorio8b4c1732011-12-06 11:28:29 -05001292 id_token=d.get('id_token', None))
Joe Gregorio695fdc12011-01-16 16:46:55 -05001293 else:
Joe Gregorioe78621a2012-03-09 15:47:23 -05001294 logger.info('Failed to retrieve access token: %s' % content)
Joe Gregorioddb969a2012-07-11 11:04:12 -04001295 if 'error' in d:
1296 # you never know what those providers got to say
1297 error_msg = unicode(d['error'])
1298 else:
1299 error_msg = 'Invalid response: %s.' % str(resp.status)
Joe Gregorioccc79542011-02-19 00:05:26 -05001300 raise FlowExchangeError(error_msg)
Joe Gregoriof08a4982011-10-07 13:11:16 -04001301
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001302
1303@util.positional(2)
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -08001304def flow_from_clientsecrets(filename, scope, redirect_uri=None,
1305 message=None, cache=None):
Joe Gregoriof08a4982011-10-07 13:11:16 -04001306 """Create a Flow from a clientsecrets file.
1307
1308 Will create the right kind of Flow based on the contents of the clientsecrets
1309 file or will raise InvalidClientSecretsError for unknown types of Flows.
1310
1311 Args:
1312 filename: string, File name of client secrets.
Joe Gregorio5cf5d122012-11-16 16:36:12 -05001313 scope: string or iterable of strings, scope(s) to request.
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001314 redirect_uri: string, Either the string 'urn:ietf:wg:oauth:2.0:oob' for
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -08001315 a non-web-based application, or a URI that handles the callback from
1316 the authorization server.
Joe Gregoriof08a4982011-10-07 13:11:16 -04001317 message: string, A friendly string to display to the user if the
1318 clientsecrets file is missing or invalid. If message is provided then
1319 sys.exit will be called in the case of an error. If message in not
1320 provided then clientsecrets.InvalidClientSecretsError will be raised.
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001321 cache: An optional cache service client that implements get() and set()
Joe Gregorioc29aaa92012-07-16 16:16:31 -04001322 methods. See clientsecrets.loadfile() for details.
Joe Gregoriof08a4982011-10-07 13:11:16 -04001323
1324 Returns:
1325 A Flow object.
1326
1327 Raises:
1328 UnknownClientSecretsFlowError if the file describes an unknown kind of Flow.
1329 clientsecrets.InvalidClientSecretsError if the clientsecrets file is
1330 invalid.
1331 """
Joe Gregorio0984ef22011-10-14 13:17:43 -04001332 try:
Joe Gregorioc29aaa92012-07-16 16:16:31 -04001333 client_type, client_info = clientsecrets.loadfile(filename, cache=cache)
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -08001334 if client_type in (clientsecrets.TYPE_WEB, clientsecrets.TYPE_INSTALLED):
1335 constructor_kwargs = {
1336 'redirect_uri': redirect_uri,
1337 'auth_uri': client_info['auth_uri'],
1338 'token_uri': client_info['token_uri'],
1339 }
1340 revoke_uri = client_info.get('revoke_uri')
1341 if revoke_uri is not None:
1342 constructor_kwargs['revoke_uri'] = revoke_uri
1343 return OAuth2WebServerFlow(
1344 client_info['client_id'], client_info['client_secret'],
1345 scope, **constructor_kwargs)
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001346
Joe Gregorio0984ef22011-10-14 13:17:43 -04001347 except clientsecrets.InvalidClientSecretsError:
1348 if message:
1349 sys.exit(message)
1350 else:
1351 raise
Joe Gregoriof08a4982011-10-07 13:11:16 -04001352 else:
1353 raise UnknownClientSecretsFlowError(
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -08001354 'This OAuth 2.0 flow is unsupported: %r' % client_type)