blob: 918539a49eccb8efde005de2eb56071b6ccb9fc5 [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 Gregoriocff6b4d2013-02-12 13:13:04 -050040HAS_OPENSSL = False
Joe Gregorio0b723c22013-01-03 15:00:50 -050041HAS_CRYPTO = False
Joe Gregorio8b4c1732011-12-06 11:28:29 -050042try:
Joe Gregorio0b723c22013-01-03 15:00:50 -050043 from oauth2client import crypt
44 HAS_CRYPTO = True
Joe Gregoriocff6b4d2013-02-12 13:13:04 -050045 if crypt.OpenSSLVerifier is not None:
46 HAS_OPENSSL = True
Joe Gregorio8b4c1732011-12-06 11:28:29 -050047except ImportError:
48 pass
49
Joe Gregorio695fdc12011-01-16 16:46:55 -050050try:
Joe Gregorio9da2ad82011-09-11 14:04:44 -040051 from urlparse import parse_qsl
Joe Gregorio695fdc12011-01-16 16:46:55 -050052except ImportError:
Joe Gregorio9da2ad82011-09-11 14:04:44 -040053 from cgi import parse_qsl
54
55logger = logging.getLogger(__name__)
Joe Gregorio695fdc12011-01-16 16:46:55 -050056
Joe Gregorio562b7312011-09-15 09:06:38 -040057# Expiry is stored in RFC3339 UTC format
Joe Gregorio8b4c1732011-12-06 11:28:29 -050058EXPIRY_FORMAT = '%Y-%m-%dT%H:%M:%SZ'
59
60# Which certs to use to validate id_tokens received.
61ID_TOKEN_VERIFICATON_CERTS = 'https://www.googleapis.com/oauth2/v1/certs'
Joe Gregorio562b7312011-09-15 09:06:38 -040062
Joe Gregoriof2326c02012-02-09 12:18:44 -050063# Constant to use for the out of band OAuth 2.0 flow.
64OOB_CALLBACK_URN = 'urn:ietf:wg:oauth:2.0:oob'
65
Joe Gregorio0bd8c412013-01-03 17:17:46 -050066# Google Data client libraries may need to set this to [401, 403].
67REFRESH_STATUS_CODES = [401]
68
Joe Gregorio695fdc12011-01-16 16:46:55 -050069
70class Error(Exception):
71 """Base error for this module."""
Joe Gregorio695fdc12011-01-16 16:46:55 -050072
73
Joe Gregorioccc79542011-02-19 00:05:26 -050074class FlowExchangeError(Error):
Joe Gregorioca876e42011-02-22 19:39:42 -050075 """Error trying to exchange an authorization grant for an access token."""
Joe Gregorioccc79542011-02-19 00:05:26 -050076
77
78class AccessTokenRefreshError(Error):
Joe Gregorioca876e42011-02-22 19:39:42 -050079 """Error trying to refresh an expired access token."""
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -080080
81
82class TokenRevokeError(Error):
83 """Error trying to revoke a token."""
84
Joe Gregorio695fdc12011-01-16 16:46:55 -050085
Joe Gregoriof08a4982011-10-07 13:11:16 -040086class UnknownClientSecretsFlowError(Error):
87 """The client secrets file called for an unknown type of OAuth 2.0 flow. """
Joe Gregoriof08a4982011-10-07 13:11:16 -040088
Joe Gregorio695fdc12011-01-16 16:46:55 -050089
Joe Gregorio3b79fa82011-02-17 11:47:17 -050090class AccessTokenCredentialsError(Error):
91 """Having only the access_token means no refresh is possible."""
Joe Gregorio695fdc12011-01-16 16:46:55 -050092
93
Joe Gregorio8b4c1732011-12-06 11:28:29 -050094class VerifyJwtTokenError(Error):
95 """Could on retrieve certificates for validation."""
Joe Gregorio8b4c1732011-12-06 11:28:29 -050096
97
Joe Gregorio83f2ee62012-12-06 15:25:54 -050098class NonAsciiHeaderError(Error):
99 """Header names and values must be ASCII strings."""
Joe Gregorio83f2ee62012-12-06 15:25:54 -0500100
101
Joe Gregorio695fdc12011-01-16 16:46:55 -0500102def _abstract():
103 raise NotImplementedError('You need to override this function')
104
105
Joe Gregorio9f2f38f2012-02-06 12:53:00 -0500106class MemoryCache(object):
107 """httplib2 Cache implementation which only caches locally."""
108
109 def __init__(self):
110 self.cache = {}
111
112 def get(self, key):
113 return self.cache.get(key)
114
115 def set(self, key, value):
116 self.cache[key] = value
117
118 def delete(self, key):
119 self.cache.pop(key, None)
120
121
Joe Gregorio695fdc12011-01-16 16:46:55 -0500122class Credentials(object):
123 """Base class for all Credentials objects.
124
Joe Gregorio562b7312011-09-15 09:06:38 -0400125 Subclasses must define an authorize() method that applies the credentials to
126 an HTTP transport.
127
128 Subclasses must also specify a classmethod named 'from_json' that takes a JSON
Joe Gregoriofa8cd9f2012-02-23 14:00:40 -0500129 string as input and returns an instaniated Credentials object.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500130 """
131
Joe Gregorio562b7312011-09-15 09:06:38 -0400132 NON_SERIALIZED_MEMBERS = ['store']
133
Joe Gregorio695fdc12011-01-16 16:46:55 -0500134 def authorize(self, http):
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800135 """Take an httplib2.Http instance (or equivalent) and authorizes it.
136
137 Authorizes it for the set of credentials, usually by replacing
138 http.request() with a method that adds in the appropriate headers and then
139 delegates to the original Http.request() method.
140
141 Args:
142 http: httplib2.Http, an http object to be used to make the refresh
143 request.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500144 """
145 _abstract()
146
Joe Gregorio654f4a22012-02-09 14:15:44 -0500147 def refresh(self, http):
148 """Forces a refresh of the access_token.
149
150 Args:
151 http: httplib2.Http, an http object to be used to make the refresh
152 request.
153 """
154 _abstract()
155
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800156 def revoke(self, http):
157 """Revokes a refresh_token and makes the credentials void.
158
159 Args:
160 http: httplib2.Http, an http object to be used to make the revoke
161 request.
162 """
163 _abstract()
164
Joe Gregorio654f4a22012-02-09 14:15:44 -0500165 def apply(self, headers):
166 """Add the authorization to the headers.
167
168 Args:
169 headers: dict, the headers to add the Authorization header to.
170 """
171 _abstract()
172
Joe Gregorio562b7312011-09-15 09:06:38 -0400173 def _to_json(self, strip):
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800174 """Utility function that creates JSON repr. of a Credentials object.
Joe Gregorio562b7312011-09-15 09:06:38 -0400175
176 Args:
177 strip: array, An array of names of members to not include in the JSON.
178
179 Returns:
180 string, a JSON representation of this instance, suitable to pass to
181 from_json().
182 """
183 t = type(self)
184 d = copy.copy(self.__dict__)
185 for member in strip:
Joe Gregorio08cdcb82012-03-14 00:09:33 -0400186 if member in d:
187 del d[member]
Joe Gregorio562b7312011-09-15 09:06:38 -0400188 if 'token_expiry' in d and isinstance(d['token_expiry'], datetime.datetime):
189 d['token_expiry'] = d['token_expiry'].strftime(EXPIRY_FORMAT)
190 # Add in information we will need later to reconsistitue this instance.
191 d['_class'] = t.__name__
192 d['_module'] = t.__module__
193 return simplejson.dumps(d)
194
195 def to_json(self):
196 """Creating a JSON representation of an instance of Credentials.
197
198 Returns:
199 string, a JSON representation of this instance, suitable to pass to
200 from_json().
201 """
202 return self._to_json(Credentials.NON_SERIALIZED_MEMBERS)
203
204 @classmethod
205 def new_from_json(cls, s):
206 """Utility class method to instantiate a Credentials subclass from a JSON
207 representation produced by to_json().
208
209 Args:
210 s: string, JSON from to_json().
211
212 Returns:
213 An instance of the subclass of Credentials that was serialized with
214 to_json().
215 """
216 data = simplejson.loads(s)
217 # Find and call the right classmethod from_json() to restore the object.
218 module = data['_module']
Joe Gregoriofa8cd9f2012-02-23 14:00:40 -0500219 try:
220 m = __import__(module)
221 except ImportError:
222 # In case there's an object from the old package structure, update it
223 module = module.replace('.apiclient', '')
224 m = __import__(module)
225
Joe Gregorioe9b40f12011-10-13 10:03:28 -0400226 m = __import__(module, fromlist=module.split('.')[:-1])
Joe Gregorio562b7312011-09-15 09:06:38 -0400227 kls = getattr(m, data['_class'])
228 from_json = getattr(kls, 'from_json')
229 return from_json(s)
230
Joe Gregorio08cdcb82012-03-14 00:09:33 -0400231 @classmethod
232 def from_json(cls, s):
Joe Gregorio401b8422012-05-03 16:35:35 -0400233 """Instantiate a Credentials object from a JSON description of it.
234
235 The JSON should have been produced by calling .to_json() on the object.
Joe Gregorio08cdcb82012-03-14 00:09:33 -0400236
237 Args:
238 data: dict, A deserialized JSON object.
239
240 Returns:
241 An instance of a Credentials subclass.
242 """
243 return Credentials()
244
JacobMoshenko8e905102011-06-20 09:53:10 -0400245
Joe Gregorio695fdc12011-01-16 16:46:55 -0500246class Flow(object):
247 """Base class for all Flow objects."""
248 pass
249
250
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500251class Storage(object):
252 """Base class for all Storage objects.
253
Joe Gregorioe2233cd2013-01-24 15:46:23 -0500254 Store and retrieve a single credential. This class supports locking
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400255 such that multiple processes and threads can operate on a single
256 store.
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500257 """
258
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400259 def acquire_lock(self):
260 """Acquires any lock necessary to access this Storage.
261
Joe Gregorio401b8422012-05-03 16:35:35 -0400262 This lock is not reentrant.
263 """
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400264 pass
265
266 def release_lock(self):
267 """Release the Storage lock.
268
269 Trying to release a lock that isn't held will result in a
270 RuntimeError.
271 """
272 pass
273
274 def locked_get(self):
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500275 """Retrieve credential.
276
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400277 The Storage lock must be held when this is called.
278
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500279 Returns:
Joe Gregorio06d852b2011-03-25 15:03:10 -0400280 oauth2client.client.Credentials
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500281 """
282 _abstract()
283
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400284 def locked_put(self, credentials):
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500285 """Write a credential.
286
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400287 The Storage lock must be held when this is called.
288
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500289 Args:
290 credentials: Credentials, the credentials to store.
291 """
292 _abstract()
293
Joe Gregorioec75dc12012-02-06 13:40:42 -0500294 def locked_delete(self):
295 """Delete a credential.
296
297 The Storage lock must be held when this is called.
298 """
299 _abstract()
300
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400301 def get(self):
302 """Retrieve credential.
303
304 The Storage lock must *not* be held when this is called.
305
306 Returns:
307 oauth2client.client.Credentials
308 """
309 self.acquire_lock()
310 try:
311 return self.locked_get()
312 finally:
313 self.release_lock()
314
315 def put(self, credentials):
316 """Write a credential.
317
318 The Storage lock must be held when this is called.
319
320 Args:
321 credentials: Credentials, the credentials to store.
322 """
323 self.acquire_lock()
324 try:
325 self.locked_put(credentials)
326 finally:
327 self.release_lock()
328
Joe Gregorioec75dc12012-02-06 13:40:42 -0500329 def delete(self):
330 """Delete credential.
331
332 Frees any resources associated with storing the credential.
333 The Storage lock must *not* be held when this is called.
334
335 Returns:
336 None
337 """
338 self.acquire_lock()
339 try:
340 return self.locked_delete()
341 finally:
342 self.release_lock()
343
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500344
Joe Gregorio83f2ee62012-12-06 15:25:54 -0500345def clean_headers(headers):
346 """Forces header keys and values to be strings, i.e not unicode.
347
348 The httplib module just concats the header keys and values in a way that may
349 make the message header a unicode string, which, if it then tries to
350 contatenate to a binary request body may result in a unicode decode error.
351
352 Args:
353 headers: dict, A dictionary of headers.
354
355 Returns:
356 The same dictionary but with all the keys converted to strings.
357 """
358 clean = {}
359 try:
360 for k, v in headers.iteritems():
361 clean[str(k)] = str(v)
362 except UnicodeEncodeError:
363 raise NonAsciiHeaderError(k + ': ' + v)
364 return clean
365
366
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800367def _update_query_params(uri, params):
368 """Updates a URI with new query parameters.
369
370 Args:
371 uri: string, A valid URI, with potential existing query parameters.
372 params: dict, A dictionary of query parameters.
373
374 Returns:
375 The same URI but with the new query parameters added.
376 """
377 parts = list(urlparse.urlparse(uri))
378 query_params = dict(parse_qsl(parts[4])) # 4 is the index of the query part
379 query_params.update(params)
380 parts[4] = urllib.urlencode(query_params)
381 return urlparse.urlunparse(parts)
382
383
Joe Gregorio695fdc12011-01-16 16:46:55 -0500384class OAuth2Credentials(Credentials):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400385 """Credentials object for OAuth 2.0.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500386
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500387 Credentials can be applied to an httplib2.Http object using the authorize()
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500388 method, which then adds the OAuth 2.0 access token to each request.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500389
390 OAuth2Credentials objects may be safely pickled and unpickled.
391 """
392
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400393 @util.positional(8)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500394 def __init__(self, access_token, client_id, client_secret, refresh_token,
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800395 token_expiry, token_uri, user_agent, revoke_uri=None,
396 id_token=None):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400397 """Create an instance of OAuth2Credentials.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500398
399 This constructor is not usually called by the user, instead
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500400 OAuth2Credentials objects are instantiated by the OAuth2WebServerFlow.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500401
402 Args:
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400403 access_token: string, access token.
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500404 client_id: string, client identifier.
405 client_secret: string, client secret.
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500406 refresh_token: string, refresh token.
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400407 token_expiry: datetime, when the access_token expires.
408 token_uri: string, URI of token endpoint.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500409 user_agent: string, The HTTP User-Agent to provide for this application.
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800410 revoke_uri: string, URI for revoke endpoint. Defaults to None; a token
411 can't be revoked if this is None.
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500412 id_token: object, The identity of the resource owner.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500413
Joe Gregorio695fdc12011-01-16 16:46:55 -0500414 Notes:
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500415 store: callable, A callable that when passed a Credential
Joe Gregorio695fdc12011-01-16 16:46:55 -0500416 will store the credential back to where it came from.
417 This is needed to store the latest access_token if it
418 has expired and been refreshed.
419 """
420 self.access_token = access_token
421 self.client_id = client_id
422 self.client_secret = client_secret
423 self.refresh_token = refresh_token
424 self.store = None
425 self.token_expiry = token_expiry
426 self.token_uri = token_uri
427 self.user_agent = user_agent
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800428 self.revoke_uri = revoke_uri
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500429 self.id_token = id_token
Joe Gregorio695fdc12011-01-16 16:46:55 -0500430
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500431 # True if the credentials have been revoked or expired and can't be
432 # refreshed.
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400433 self.invalid = False
Joe Gregorio9ce4b622011-02-17 15:32:11 -0500434
Joe Gregorio654f4a22012-02-09 14:15:44 -0500435 def authorize(self, http):
436 """Authorize an httplib2.Http instance with these credentials.
437
438 The modified http.request method will add authentication headers to each
439 request and will refresh access_tokens when a 401 is received on a
440 request. In addition the http.request method has a credentials property,
441 http.request.credentials, which is the Credentials object that authorized
442 it.
443
444 Args:
445 http: An instance of httplib2.Http
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800446 or something that acts like it.
Joe Gregorio654f4a22012-02-09 14:15:44 -0500447
448 Returns:
449 A modified instance of http that was passed in.
450
451 Example:
452
453 h = httplib2.Http()
454 h = credentials.authorize(h)
455
456 You can't create a new OAuth subclass of httplib2.Authenication
457 because it never gets passed the absolute URI, which is needed for
458 signing. So instead we have to overload 'request' with a closure
459 that adds in the Authorization header and then calls the original
460 version of 'request()'.
461 """
462 request_orig = http.request
463
464 # The closure that will replace 'httplib2.Http.request'.
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400465 @util.positional(1)
Joe Gregorio654f4a22012-02-09 14:15:44 -0500466 def new_request(uri, method='GET', body=None, headers=None,
467 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
468 connection_type=None):
469 if not self.access_token:
470 logger.info('Attempting refresh to obtain initial access_token')
471 self._refresh(request_orig)
472
473 # Modify the request headers to add the appropriate
474 # Authorization header.
475 if headers is None:
476 headers = {}
477 self.apply(headers)
478
479 if self.user_agent is not None:
480 if 'user-agent' in headers:
481 headers['user-agent'] = self.user_agent + ' ' + headers['user-agent']
482 else:
483 headers['user-agent'] = self.user_agent
484
Joe Gregorio83f2ee62012-12-06 15:25:54 -0500485 resp, content = request_orig(uri, method, body, clean_headers(headers),
Joe Gregorio654f4a22012-02-09 14:15:44 -0500486 redirections, connection_type)
487
Joe Gregorio0bd8c412013-01-03 17:17:46 -0500488 if resp.status in REFRESH_STATUS_CODES:
Joe Gregorio7c7c6b12012-07-16 16:31:01 -0400489 logger.info('Refreshing due to a %s' % str(resp.status))
Joe Gregorio654f4a22012-02-09 14:15:44 -0500490 self._refresh(request_orig)
491 self.apply(headers)
Joe Gregorio83f2ee62012-12-06 15:25:54 -0500492 return request_orig(uri, method, body, clean_headers(headers),
Joe Gregorio654f4a22012-02-09 14:15:44 -0500493 redirections, connection_type)
494 else:
495 return (resp, content)
496
497 # Replace the request method with our own closure.
498 http.request = new_request
499
500 # Set credentials as a property of the request method.
501 setattr(http.request, 'credentials', self)
502
503 return http
504
505 def refresh(self, http):
506 """Forces a refresh of the access_token.
507
508 Args:
509 http: httplib2.Http, an http object to be used to make the refresh
510 request.
511 """
512 self._refresh(http.request)
513
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800514 def revoke(self, http):
515 """Revokes a refresh_token and makes the credentials void.
516
517 Args:
518 http: httplib2.Http, an http object to be used to make the revoke
519 request.
520 """
521 self._revoke(http.request)
522
Joe Gregorio654f4a22012-02-09 14:15:44 -0500523 def apply(self, headers):
524 """Add the authorization to the headers.
525
526 Args:
527 headers: dict, the headers to add the Authorization header to.
528 """
529 headers['Authorization'] = 'Bearer ' + self.access_token
530
Joe Gregorio562b7312011-09-15 09:06:38 -0400531 def to_json(self):
532 return self._to_json(Credentials.NON_SERIALIZED_MEMBERS)
533
534 @classmethod
535 def from_json(cls, s):
536 """Instantiate a Credentials object from a JSON description of it. The JSON
537 should have been produced by calling .to_json() on the object.
538
539 Args:
540 data: dict, A deserialized JSON object.
541
542 Returns:
543 An instance of a Credentials subclass.
544 """
545 data = simplejson.loads(s)
546 if 'token_expiry' in data and not isinstance(data['token_expiry'],
547 datetime.datetime):
Joe Gregorio1daa71b2011-09-15 18:12:14 -0400548 try:
549 data['token_expiry'] = datetime.datetime.strptime(
550 data['token_expiry'], EXPIRY_FORMAT)
551 except:
552 data['token_expiry'] = None
Joe Gregorio562b7312011-09-15 09:06:38 -0400553 retval = OAuth2Credentials(
554 data['access_token'],
555 data['client_id'],
556 data['client_secret'],
557 data['refresh_token'],
558 data['token_expiry'],
559 data['token_uri'],
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500560 data['user_agent'],
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800561 revoke_uri=data.get('revoke_uri', None),
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400562 id_token=data.get('id_token', None))
Joe Gregorio562b7312011-09-15 09:06:38 -0400563 retval.invalid = data['invalid']
564 return retval
565
Joe Gregorio9ce4b622011-02-17 15:32:11 -0500566 @property
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400567 def access_token_expired(self):
568 """True if the credential is expired or invalid.
569
570 If the token_expiry isn't set, we assume the token doesn't expire.
571 """
572 if self.invalid:
573 return True
574
575 if not self.token_expiry:
576 return False
577
Joe Gregorio562b7312011-09-15 09:06:38 -0400578 now = datetime.datetime.utcnow()
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400579 if now >= self.token_expiry:
580 logger.info('access_token is expired. Now: %s, token_expiry: %s',
581 now, self.token_expiry)
582 return True
583 return False
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500584
Joe Gregorio695fdc12011-01-16 16:46:55 -0500585 def set_store(self, store):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400586 """Set the Storage for the credential.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500587
588 Args:
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400589 store: Storage, an implementation of Stroage object.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500590 This is needed to store the latest access_token if it
Joe Gregorioe2233cd2013-01-24 15:46:23 -0500591 has expired and been refreshed. This implementation uses
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400592 locking to check for updates before updating the
593 access_token.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500594 """
595 self.store = store
596
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400597 def _updateFromCredential(self, other):
598 """Update this Credential from another instance."""
599 self.__dict__.update(other.__getstate__())
600
Joe Gregorio695fdc12011-01-16 16:46:55 -0500601 def __getstate__(self):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400602 """Trim the state down to something that can be pickled."""
Joe Gregorio695fdc12011-01-16 16:46:55 -0500603 d = copy.copy(self.__dict__)
604 del d['store']
605 return d
606
607 def __setstate__(self, state):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400608 """Reconstitute the state of the object from being pickled."""
Joe Gregorio695fdc12011-01-16 16:46:55 -0500609 self.__dict__.update(state)
610 self.store = None
611
JacobMoshenko8e905102011-06-20 09:53:10 -0400612 def _generate_refresh_request_body(self):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400613 """Generate the body that will be used in the refresh request."""
JacobMoshenko8e905102011-06-20 09:53:10 -0400614 body = urllib.urlencode({
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400615 'grant_type': 'refresh_token',
616 'client_id': self.client_id,
617 'client_secret': self.client_secret,
618 'refresh_token': self.refresh_token,
619 })
JacobMoshenko8e905102011-06-20 09:53:10 -0400620 return body
621
622 def _generate_refresh_request_headers(self):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400623 """Generate the headers that will be used in the refresh request."""
JacobMoshenko8e905102011-06-20 09:53:10 -0400624 headers = {
JacobMoshenko8e905102011-06-20 09:53:10 -0400625 'content-type': 'application/x-www-form-urlencoded',
626 }
JacobMoshenkocb6d8912011-07-08 13:35:15 -0400627
628 if self.user_agent is not None:
629 headers['user-agent'] = self.user_agent
630
JacobMoshenko8e905102011-06-20 09:53:10 -0400631 return headers
632
Joe Gregorio695fdc12011-01-16 16:46:55 -0500633 def _refresh(self, http_request):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400634 """Refreshes the access_token.
635
636 This method first checks by reading the Storage object if available.
637 If a refresh is still needed, it holds the Storage lock until the
638 refresh is completed.
Joe Gregorio654f4a22012-02-09 14:15:44 -0500639
640 Args:
641 http_request: callable, a callable that matches the method signature of
642 httplib2.Http.request, used to make the refresh request.
643
644 Raises:
645 AccessTokenRefreshError: When the refresh fails.
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400646 """
647 if not self.store:
648 self._do_refresh_request(http_request)
649 else:
650 self.store.acquire_lock()
651 try:
652 new_cred = self.store.locked_get()
653 if (new_cred and not new_cred.invalid and
654 new_cred.access_token != self.access_token):
655 logger.info('Updated access_token read from Storage')
656 self._updateFromCredential(new_cred)
657 else:
658 self._do_refresh_request(http_request)
659 finally:
660 self.store.release_lock()
661
662 def _do_refresh_request(self, http_request):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500663 """Refresh the access_token using the refresh_token.
664
665 Args:
Joe Gregorio654f4a22012-02-09 14:15:44 -0500666 http_request: callable, a callable that matches the method signature of
667 httplib2.Http.request, used to make the refresh request.
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400668
669 Raises:
670 AccessTokenRefreshError: When the refresh fails.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500671 """
JacobMoshenko8e905102011-06-20 09:53:10 -0400672 body = self._generate_refresh_request_body()
673 headers = self._generate_refresh_request_headers()
674
Joe Gregorio4b4002f2012-06-14 15:41:01 -0400675 logger.info('Refreshing access_token')
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500676 resp, content = http_request(
677 self.token_uri, method='POST', body=body, headers=headers)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500678 if resp.status == 200:
679 # TODO(jcgregorio) Raise an error if loads fails?
680 d = simplejson.loads(content)
681 self.access_token = d['access_token']
682 self.refresh_token = d.get('refresh_token', self.refresh_token)
683 if 'expires_in' in d:
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500684 self.token_expiry = datetime.timedelta(
Joe Gregorio562b7312011-09-15 09:06:38 -0400685 seconds=int(d['expires_in'])) + datetime.datetime.utcnow()
Joe Gregorio695fdc12011-01-16 16:46:55 -0500686 else:
687 self.token_expiry = None
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400688 if self.store:
689 self.store.locked_put(self)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500690 else:
JacobMoshenko8e905102011-06-20 09:53:10 -0400691 # An {'error':...} response body means the token is expired or revoked,
692 # so we flag the credentials as such.
Joe Gregorioe78621a2012-03-09 15:47:23 -0500693 logger.info('Failed to retrieve access token: %s' % content)
Joe Gregorioccc79542011-02-19 00:05:26 -0500694 error_msg = 'Invalid response %s.' % resp['status']
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500695 try:
696 d = simplejson.loads(content)
697 if 'error' in d:
Joe Gregorioccc79542011-02-19 00:05:26 -0500698 error_msg = d['error']
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400699 self.invalid = True
700 if self.store:
701 self.store.locked_put(self)
Joe Gregoriofd08e432012-08-09 14:17:41 -0400702 except StandardError:
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500703 pass
Joe Gregorioccc79542011-02-19 00:05:26 -0500704 raise AccessTokenRefreshError(error_msg)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500705
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800706 def _revoke(self, http_request):
707 """Revokes the refresh_token and deletes the store if available.
708
709 Args:
710 http_request: callable, a callable that matches the method signature of
711 httplib2.Http.request, used to make the revoke request.
712 """
713 self._do_revoke(http_request, self.refresh_token)
714
715 def _do_revoke(self, http_request, token):
716 """Revokes the credentials and deletes the store if available.
717
718 Args:
719 http_request: callable, a callable that matches the method signature of
720 httplib2.Http.request, used to make the refresh request.
721 token: A string used as the token to be revoked. Can be either an
722 access_token or refresh_token.
723
724 Raises:
725 TokenRevokeError: If the revoke request does not return with a 200 OK.
726 """
727 logger.info('Revoking token')
728 query_params = {'token': token}
729 token_revoke_uri = _update_query_params(self.revoke_uri, query_params)
730 resp, content = http_request(token_revoke_uri)
731 if resp.status == 200:
732 self.invalid = True
733 else:
734 error_msg = 'Invalid response %s.' % resp.status
735 try:
736 d = simplejson.loads(content)
737 if 'error' in d:
738 error_msg = d['error']
739 except StandardError:
740 pass
741 raise TokenRevokeError(error_msg)
742
743 if self.store:
744 self.store.delete()
745
Joe Gregorio695fdc12011-01-16 16:46:55 -0500746
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500747class AccessTokenCredentials(OAuth2Credentials):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400748 """Credentials object for OAuth 2.0.
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500749
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400750 Credentials can be applied to an httplib2.Http object using the
751 authorize() method, which then signs each request from that object
Joe Gregorioe2233cd2013-01-24 15:46:23 -0500752 with the OAuth 2.0 access token. This set of credentials is for the
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400753 use case where you have acquired an OAuth 2.0 access_token from
754 another place such as a JavaScript client or another web
755 application, and wish to use it from Python. Because only the
756 access_token is present it can not be refreshed and will in time
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500757 expire.
758
Joe Gregorio9ce4b622011-02-17 15:32:11 -0500759 AccessTokenCredentials objects may be safely pickled and unpickled.
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500760
761 Usage:
762 credentials = AccessTokenCredentials('<an access token>',
763 'my-user-agent/1.0')
764 http = httplib2.Http()
765 http = credentials.authorize(http)
766
767 Exceptions:
768 AccessTokenCredentialsExpired: raised when the access_token expires or is
769 revoked.
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500770 """
771
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800772 def __init__(self, access_token, user_agent, revoke_uri=None):
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500773 """Create an instance of OAuth2Credentials
774
775 This is one of the few types if Credentials that you should contrust,
776 Credentials objects are usually instantiated by a Flow.
777
778 Args:
ade@google.com93a7f7c2011-02-23 16:00:37 +0000779 access_token: string, access token.
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500780 user_agent: string, The HTTP User-Agent to provide for this application.
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800781 revoke_uri: string, URI for revoke endpoint. Defaults to None; a token
782 can't be revoked if this is None.
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500783 """
784 super(AccessTokenCredentials, self).__init__(
785 access_token,
786 None,
787 None,
788 None,
789 None,
790 None,
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800791 user_agent,
792 revoke_uri=revoke_uri)
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500793
Joe Gregorio562b7312011-09-15 09:06:38 -0400794
795 @classmethod
796 def from_json(cls, s):
797 data = simplejson.loads(s)
798 retval = AccessTokenCredentials(
799 data['access_token'],
800 data['user_agent'])
801 return retval
802
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500803 def _refresh(self, http_request):
804 raise AccessTokenCredentialsError(
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800805 'The access_token is expired or invalid and can\'t be refreshed.')
806
807 def _revoke(self, http_request):
808 """Revokes the access_token and deletes the store if available.
809
810 Args:
811 http_request: callable, a callable that matches the method signature of
812 httplib2.Http.request, used to make the revoke request.
813 """
814 self._do_revoke(http_request, self.access_token)
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500815
JacobMoshenko8e905102011-06-20 09:53:10 -0400816
817class AssertionCredentials(OAuth2Credentials):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400818 """Abstract Credentials object used for OAuth 2.0 assertion grants.
JacobMoshenko8e905102011-06-20 09:53:10 -0400819
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400820 This credential does not require a flow to instantiate because it
821 represents a two legged flow, and therefore has all of the required
Joe Gregorioe2233cd2013-01-24 15:46:23 -0500822 information to generate and refresh its own access tokens. It must
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400823 be subclassed to generate the appropriate assertion string.
JacobMoshenko8e905102011-06-20 09:53:10 -0400824
825 AssertionCredentials objects may be safely pickled and unpickled.
826 """
827
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400828 @util.positional(2)
829 def __init__(self, assertion_type, user_agent=None,
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800830 token_uri=GOOGLE_TOKEN_URI,
831 revoke_uri=GOOGLE_REVOKE_URI,
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400832 **unused_kwargs):
833 """Constructor for AssertionFlowCredentials.
JacobMoshenko8e905102011-06-20 09:53:10 -0400834
835 Args:
836 assertion_type: string, assertion type that will be declared to the auth
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800837 server
JacobMoshenko8e905102011-06-20 09:53:10 -0400838 user_agent: string, The HTTP User-Agent to provide for this application.
839 token_uri: string, URI for token endpoint. For convenience
840 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800841 revoke_uri: string, URI for revoke endpoint.
JacobMoshenko8e905102011-06-20 09:53:10 -0400842 """
843 super(AssertionCredentials, self).__init__(
844 None,
845 None,
846 None,
847 None,
848 None,
849 token_uri,
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800850 user_agent,
851 revoke_uri=revoke_uri)
JacobMoshenko8e905102011-06-20 09:53:10 -0400852 self.assertion_type = assertion_type
853
854 def _generate_refresh_request_body(self):
855 assertion = self._generate_assertion()
856
857 body = urllib.urlencode({
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400858 'assertion': assertion,
Joe Gregoriocdc350f2013-02-07 10:52:26 -0500859 'grant_type': 'urn:ietf:params:oauth:grant-type:jwt-bearer',
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400860 })
JacobMoshenko8e905102011-06-20 09:53:10 -0400861
862 return body
863
864 def _generate_assertion(self):
865 """Generate the assertion string that will be used in the access token
866 request.
867 """
868 _abstract()
869
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800870 def _revoke(self, http_request):
871 """Revokes the access_token and deletes the store if available.
872
873 Args:
874 http_request: callable, a callable that matches the method signature of
875 httplib2.Http.request, used to make the revoke request.
876 """
877 self._do_revoke(http_request, self.access_token)
878
879
Joe Gregorio0b723c22013-01-03 15:00:50 -0500880if HAS_CRYPTO:
881 # PyOpenSSL and PyCrypto are not prerequisites for oauth2client, so if it is
882 # missing then don't create the SignedJwtAssertionCredentials or the
883 # verify_id_token() method.
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500884
885 class SignedJwtAssertionCredentials(AssertionCredentials):
886 """Credentials object used for OAuth 2.0 Signed JWT assertion grants.
887
Joe Gregorio672051e2012-07-10 09:11:45 -0400888 This credential does not require a flow to instantiate because it represents
889 a two legged flow, and therefore has all of the required information to
890 generate and refresh its own access tokens.
891
Joe Gregorio0b723c22013-01-03 15:00:50 -0500892 SignedJwtAssertionCredentials requires either PyOpenSSL, or PyCrypto 2.6 or
893 later. For App Engine you may also consider using AppAssertionCredentials.
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500894 """
895
896 MAX_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds
897
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400898 @util.positional(4)
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500899 def __init__(self,
900 service_account_name,
901 private_key,
902 scope,
903 private_key_password='notasecret',
904 user_agent=None,
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800905 token_uri=GOOGLE_TOKEN_URI,
906 revoke_uri=GOOGLE_REVOKE_URI,
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500907 **kwargs):
908 """Constructor for SignedJwtAssertionCredentials.
909
910 Args:
911 service_account_name: string, id for account, usually an email address.
Joe Gregorio0b723c22013-01-03 15:00:50 -0500912 private_key: string, private key in PKCS12 or PEM format.
Joe Gregorio5cf5d122012-11-16 16:36:12 -0500913 scope: string or iterable of strings, scope(s) of the credentials being
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500914 requested.
Joe Gregorio0b723c22013-01-03 15:00:50 -0500915 private_key_password: string, password for private_key, unused if
916 private_key is in PEM format.
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500917 user_agent: string, HTTP User-Agent to provide for this application.
918 token_uri: string, URI for token endpoint. For convenience
919 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800920 revoke_uri: string, URI for revoke endpoint.
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500921 kwargs: kwargs, Additional parameters to add to the JWT token, for
922 example prn=joe@xample.org."""
923
924 super(SignedJwtAssertionCredentials, self).__init__(
dhermes@google.com2cc09382013-02-11 08:42:18 -0800925 None,
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400926 user_agent=user_agent,
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500927 token_uri=token_uri,
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800928 revoke_uri=revoke_uri,
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500929 )
930
Joe Gregorio5cf5d122012-11-16 16:36:12 -0500931 self.scope = util.scopes_to_string(scope)
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500932
Joe Gregorioa19f3a72012-07-11 15:35:35 -0400933 # Keep base64 encoded so it can be stored in JSON.
934 self.private_key = base64.b64encode(private_key)
935
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500936 self.private_key_password = private_key_password
937 self.service_account_name = service_account_name
938 self.kwargs = kwargs
939
940 @classmethod
941 def from_json(cls, s):
942 data = simplejson.loads(s)
943 retval = SignedJwtAssertionCredentials(
944 data['service_account_name'],
Joe Gregorioa19f3a72012-07-11 15:35:35 -0400945 base64.b64decode(data['private_key']),
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500946 data['scope'],
Joe Gregorioa19f3a72012-07-11 15:35:35 -0400947 private_key_password=data['private_key_password'],
948 user_agent=data['user_agent'],
949 token_uri=data['token_uri'],
950 **data['kwargs']
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500951 )
952 retval.invalid = data['invalid']
Joe Gregorioa19f3a72012-07-11 15:35:35 -0400953 retval.access_token = data['access_token']
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500954 return retval
955
956 def _generate_assertion(self):
957 """Generate the assertion that will be used in the request."""
958 now = long(time.time())
959 payload = {
960 'aud': self.token_uri,
961 'scope': self.scope,
962 'iat': now,
963 'exp': now + SignedJwtAssertionCredentials.MAX_TOKEN_LIFETIME_SECS,
964 'iss': self.service_account_name
965 }
966 payload.update(self.kwargs)
Joe Gregorioe78621a2012-03-09 15:47:23 -0500967 logger.debug(str(payload))
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500968
Joe Gregorioa19f3a72012-07-11 15:35:35 -0400969 private_key = base64.b64decode(self.private_key)
Joe Gregorio0b723c22013-01-03 15:00:50 -0500970 return crypt.make_signed_jwt(crypt.Signer.from_string(
971 private_key, self.private_key_password), payload)
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500972
Joe Gregorio9f2f38f2012-02-06 12:53:00 -0500973 # Only used in verify_id_token(), which is always calling to the same URI
974 # for the certs.
975 _cached_http = httplib2.Http(MemoryCache())
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500976
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400977 @util.positional(2)
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500978 def verify_id_token(id_token, audience, http=None,
979 cert_uri=ID_TOKEN_VERIFICATON_CERTS):
980 """Verifies a signed JWT id_token.
981
Joe Gregorio672051e2012-07-10 09:11:45 -0400982 This function requires PyOpenSSL and because of that it does not work on
Joe Gregorio0b723c22013-01-03 15:00:50 -0500983 App Engine.
Joe Gregorio672051e2012-07-10 09:11:45 -0400984
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500985 Args:
986 id_token: string, A Signed JWT.
987 audience: string, The audience 'aud' that the token should be for.
988 http: httplib2.Http, instance to use to make the HTTP request. Callers
989 should supply an instance that has caching enabled.
990 cert_uri: string, URI of the certificates in JSON format to
991 verify the JWT against.
992
993 Returns:
994 The deserialized JSON in the JWT.
995
996 Raises:
997 oauth2client.crypt.AppIdentityError if the JWT fails to verify.
998 """
999 if http is None:
Joe Gregorio9f2f38f2012-02-06 12:53:00 -05001000 http = _cached_http
Joe Gregorio8b4c1732011-12-06 11:28:29 -05001001
1002 resp, content = http.request(cert_uri)
1003
1004 if resp.status == 200:
1005 certs = simplejson.loads(content)
Joe Gregorio0b723c22013-01-03 15:00:50 -05001006 return crypt.verify_signed_jwt_with_certs(id_token, certs, audience)
Joe Gregorio8b4c1732011-12-06 11:28:29 -05001007 else:
1008 raise VerifyJwtTokenError('Status code: %d' % resp.status)
1009
1010
1011def _urlsafe_b64decode(b64string):
Joe Gregoriobd512b52011-12-06 15:39:26 -05001012 # Guard against unicode strings, which base64 can't handle.
1013 b64string = b64string.encode('ascii')
Joe Gregorio8b4c1732011-12-06 11:28:29 -05001014 padded = b64string + '=' * (4 - len(b64string) % 4)
1015 return base64.urlsafe_b64decode(padded)
1016
1017
1018def _extract_id_token(id_token):
1019 """Extract the JSON payload from a JWT.
1020
1021 Does the extraction w/o checking the signature.
1022
1023 Args:
1024 id_token: string, OAuth 2.0 id_token.
1025
1026 Returns:
1027 object, The deserialized JSON payload.
1028 """
1029 segments = id_token.split('.')
1030
1031 if (len(segments) != 3):
1032 raise VerifyJwtTokenError(
1033 'Wrong number of segments in token: %s' % id_token)
1034
1035 return simplejson.loads(_urlsafe_b64decode(segments[1]))
1036
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001037
Joe Gregorioddb969a2012-07-11 11:04:12 -04001038def _parse_exchange_token_response(content):
1039 """Parses response of an exchange token request.
1040
1041 Most providers return JSON but some (e.g. Facebook) return a
1042 url-encoded string.
1043
1044 Args:
1045 content: The body of a response
1046
1047 Returns:
1048 Content as a dictionary object. Note that the dict could be empty,
1049 i.e. {}. That basically indicates a failure.
1050 """
1051 resp = {}
1052 try:
1053 resp = simplejson.loads(content)
1054 except StandardError:
1055 # different JSON libs raise different exceptions,
1056 # so we just do a catch-all here
1057 resp = dict(parse_qsl(content))
1058
1059 # some providers respond with 'expires', others with 'expires_in'
1060 if resp and 'expires' in resp:
1061 resp['expires_in'] = resp.pop('expires')
1062
1063 return resp
1064
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001065
1066@util.positional(4)
Joe Gregorio32d852d2012-06-14 09:08:18 -04001067def credentials_from_code(client_id, client_secret, scope, code,
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -08001068 redirect_uri='postmessage', http=None,
1069 user_agent=None, token_uri=GOOGLE_TOKEN_URI,
1070 auth_uri=GOOGLE_AUTH_URI,
1071 revoke_uri=GOOGLE_REVOKE_URI):
Joe Gregorio32d852d2012-06-14 09:08:18 -04001072 """Exchanges an authorization code for an OAuth2Credentials object.
1073
1074 Args:
1075 client_id: string, client identifier.
1076 client_secret: string, client secret.
Joe Gregorio5cf5d122012-11-16 16:36:12 -05001077 scope: string or iterable of strings, scope(s) to request.
Joe Gregorio32d852d2012-06-14 09:08:18 -04001078 code: string, An authroization code, most likely passed down from
1079 the client
1080 redirect_uri: string, this is generally set to 'postmessage' to match the
1081 redirect_uri that the client specified
1082 http: httplib2.Http, optional http instance to use to do the fetch
1083 token_uri: string, URI for token endpoint. For convenience
1084 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -08001085 auth_uri: string, URI for authorization endpoint. For convenience
1086 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
1087 revoke_uri: string, URI for revoke endpoint. For convenience
1088 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
1089
Joe Gregorio32d852d2012-06-14 09:08:18 -04001090 Returns:
1091 An OAuth2Credentials object.
1092
1093 Raises:
1094 FlowExchangeError if the authorization code cannot be exchanged for an
1095 access token
1096 """
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001097 flow = OAuth2WebServerFlow(client_id, client_secret, scope,
1098 redirect_uri=redirect_uri, user_agent=user_agent,
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -08001099 auth_uri=auth_uri, token_uri=token_uri,
1100 revoke_uri=revoke_uri)
Joe Gregorio32d852d2012-06-14 09:08:18 -04001101
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001102 credentials = flow.step2_exchange(code, http=http)
Joe Gregorio32d852d2012-06-14 09:08:18 -04001103 return credentials
1104
1105
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001106@util.positional(3)
Joe Gregorio32d852d2012-06-14 09:08:18 -04001107def credentials_from_clientsecrets_and_code(filename, scope, code,
1108 message = None,
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001109 redirect_uri='postmessage',
Joe Gregorioc29aaa92012-07-16 16:16:31 -04001110 http=None,
1111 cache=None):
Joe Gregorio32d852d2012-06-14 09:08:18 -04001112 """Returns OAuth2Credentials from a clientsecrets file and an auth code.
1113
1114 Will create the right kind of Flow based on the contents of the clientsecrets
1115 file or will raise InvalidClientSecretsError for unknown types of Flows.
1116
1117 Args:
1118 filename: string, File name of clientsecrets.
Joe Gregorio5cf5d122012-11-16 16:36:12 -05001119 scope: string or iterable of strings, scope(s) to request.
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001120 code: string, An authorization code, most likely passed down from
Joe Gregorio32d852d2012-06-14 09:08:18 -04001121 the client
1122 message: string, A friendly string to display to the user if the
1123 clientsecrets file is missing or invalid. If message is provided then
1124 sys.exit will be called in the case of an error. If message in not
1125 provided then clientsecrets.InvalidClientSecretsError will be raised.
1126 redirect_uri: string, this is generally set to 'postmessage' to match the
1127 redirect_uri that the client specified
1128 http: httplib2.Http, optional http instance to use to do the fetch
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001129 cache: An optional cache service client that implements get() and set()
Joe Gregorioc29aaa92012-07-16 16:16:31 -04001130 methods. See clientsecrets.loadfile() for details.
Joe Gregorio32d852d2012-06-14 09:08:18 -04001131
1132 Returns:
1133 An OAuth2Credentials object.
1134
1135 Raises:
1136 FlowExchangeError if the authorization code cannot be exchanged for an
1137 access token
1138 UnknownClientSecretsFlowError if the file describes an unknown kind of Flow.
1139 clientsecrets.InvalidClientSecretsError if the clientsecrets file is
1140 invalid.
1141 """
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001142 flow = flow_from_clientsecrets(filename, scope, message=message, cache=cache,
1143 redirect_uri=redirect_uri)
1144 credentials = flow.step2_exchange(code, http=http)
Joe Gregorio32d852d2012-06-14 09:08:18 -04001145 return credentials
1146
JacobMoshenko8e905102011-06-20 09:53:10 -04001147
Joe Gregorio695fdc12011-01-16 16:46:55 -05001148class OAuth2WebServerFlow(Flow):
1149 """Does the Web Server Flow for OAuth 2.0.
1150
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001151 OAuth2WebServerFlow objects may be safely pickled and unpickled.
Joe Gregorio695fdc12011-01-16 16:46:55 -05001152 """
1153
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001154 @util.positional(4)
1155 def __init__(self, client_id, client_secret, scope,
1156 redirect_uri=None,
1157 user_agent=None,
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -08001158 auth_uri=GOOGLE_AUTH_URI,
1159 token_uri=GOOGLE_TOKEN_URI,
1160 revoke_uri=GOOGLE_REVOKE_URI,
Joe Gregorio9da2ad82011-09-11 14:04:44 -04001161 **kwargs):
1162 """Constructor for OAuth2WebServerFlow.
Joe Gregorio695fdc12011-01-16 16:46:55 -05001163
Joe Gregorio32f73192012-10-23 16:13:44 -04001164 The kwargs argument is used to set extra query parameters on the
1165 auth_uri. For example, the access_type and approval_prompt
1166 query parameters can be set via kwargs.
1167
Joe Gregorio695fdc12011-01-16 16:46:55 -05001168 Args:
Joe Gregorio7c22ab22011-02-16 15:32:39 -05001169 client_id: string, client identifier.
1170 client_secret: string client secret.
Joe Gregorio5cf5d122012-11-16 16:36:12 -05001171 scope: string or iterable of strings, scope(s) of the credentials being
Joe Gregoriof2f8a5a2011-10-14 15:11:29 -04001172 requested.
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001173 redirect_uri: string, Either the string 'urn:ietf:wg:oauth:2.0:oob' for
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -08001174 a non-web-based application, or a URI that handles the callback from
1175 the authorization server.
Joe Gregorio695fdc12011-01-16 16:46:55 -05001176 user_agent: string, HTTP User-Agent to provide for this application.
Joe Gregorio7c22ab22011-02-16 15:32:39 -05001177 auth_uri: string, URI for authorization endpoint. For convenience
1178 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
1179 token_uri: string, URI for token endpoint. For convenience
1180 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -08001181 revoke_uri: string, URI for revoke endpoint. For convenience
1182 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
Joe Gregorio695fdc12011-01-16 16:46:55 -05001183 **kwargs: dict, The keyword arguments are all optional and required
1184 parameters for the OAuth calls.
1185 """
1186 self.client_id = client_id
1187 self.client_secret = client_secret
Joe Gregorio5cf5d122012-11-16 16:36:12 -05001188 self.scope = util.scopes_to_string(scope)
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001189 self.redirect_uri = redirect_uri
Joe Gregorio695fdc12011-01-16 16:46:55 -05001190 self.user_agent = user_agent
Joe Gregorio7c22ab22011-02-16 15:32:39 -05001191 self.auth_uri = auth_uri
Joe Gregorio695fdc12011-01-16 16:46:55 -05001192 self.token_uri = token_uri
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -08001193 self.revoke_uri = revoke_uri
Joe Gregorio69a0aca2011-11-03 10:47:32 -04001194 self.params = {
1195 'access_type': 'offline',
Joe Gregorio32f73192012-10-23 16:13:44 -04001196 'response_type': 'code',
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -08001197 }
Joe Gregorio69a0aca2011-11-03 10:47:32 -04001198 self.params.update(kwargs)
Joe Gregorio695fdc12011-01-16 16:46:55 -05001199
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001200 @util.positional(1)
1201 def step1_get_authorize_url(self, redirect_uri=None):
Joe Gregorio695fdc12011-01-16 16:46:55 -05001202 """Returns a URI to redirect to the provider.
1203
1204 Args:
Joe Gregoriof2326c02012-02-09 12:18:44 -05001205 redirect_uri: string, Either the string 'urn:ietf:wg:oauth:2.0:oob' for
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -08001206 a non-web-based application, or a URI that handles the callback from
1207 the authorization server. This parameter is deprecated, please move to
1208 passing the redirect_uri in via the constructor.
Joe Gregorio695fdc12011-01-16 16:46:55 -05001209
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001210 Returns:
1211 A URI as a string to redirect the user to begin the authorization flow.
Joe Gregorio695fdc12011-01-16 16:46:55 -05001212 """
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001213 if redirect_uri is not None:
1214 logger.warning(('The redirect_uri parameter for'
1215 'OAuth2WebServerFlow.step1_get_authorize_url is deprecated. Please'
1216 'move to passing the redirect_uri in via the constructor.'))
1217 self.redirect_uri = redirect_uri
Joe Gregorio695fdc12011-01-16 16:46:55 -05001218
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001219 if self.redirect_uri is None:
1220 raise ValueError('The value of redirect_uri must not be None.')
1221
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -08001222 query_params = {
Joe Gregorio9da2ad82011-09-11 14:04:44 -04001223 'client_id': self.client_id,
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001224 'redirect_uri': self.redirect_uri,
Joe Gregorio9da2ad82011-09-11 14:04:44 -04001225 'scope': self.scope,
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -08001226 }
1227 query_params.update(self.params)
1228 return _update_query_params(self.auth_uri, query_params)
Joe Gregorio695fdc12011-01-16 16:46:55 -05001229
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001230 @util.positional(2)
Joe Gregorioccc79542011-02-19 00:05:26 -05001231 def step2_exchange(self, code, http=None):
Joe Gregorio695fdc12011-01-16 16:46:55 -05001232 """Exhanges a code for OAuth2Credentials.
1233
1234 Args:
1235 code: string or dict, either the code as a string, or a dictionary
1236 of the query parameters to the redirect_uri, which contains
1237 the code.
Joe Gregorioccc79542011-02-19 00:05:26 -05001238 http: httplib2.Http, optional http instance to use to do the fetch
Joe Gregorio4b4002f2012-06-14 15:41:01 -04001239
1240 Returns:
1241 An OAuth2Credentials object that can be used to authorize requests.
1242
1243 Raises:
1244 FlowExchangeError if a problem occured exchanging the code for a
1245 refresh_token.
Joe Gregorio695fdc12011-01-16 16:46:55 -05001246 """
1247
1248 if not (isinstance(code, str) or isinstance(code, unicode)):
Joe Gregorio4b4002f2012-06-14 15:41:01 -04001249 if 'code' not in code:
1250 if 'error' in code:
1251 error_msg = code['error']
1252 else:
1253 error_msg = 'No code was supplied in the query parameters.'
1254 raise FlowExchangeError(error_msg)
1255 else:
1256 code = code['code']
Joe Gregorio695fdc12011-01-16 16:46:55 -05001257
1258 body = urllib.urlencode({
Joe Gregorio9da2ad82011-09-11 14:04:44 -04001259 'grant_type': 'authorization_code',
1260 'client_id': self.client_id,
1261 'client_secret': self.client_secret,
1262 'code': code,
1263 'redirect_uri': self.redirect_uri,
1264 'scope': self.scope,
1265 })
Joe Gregorio695fdc12011-01-16 16:46:55 -05001266 headers = {
Joe Gregorio9da2ad82011-09-11 14:04:44 -04001267 'content-type': 'application/x-www-form-urlencoded',
Joe Gregorio695fdc12011-01-16 16:46:55 -05001268 }
JacobMoshenkocb6d8912011-07-08 13:35:15 -04001269
1270 if self.user_agent is not None:
1271 headers['user-agent'] = self.user_agent
1272
Joe Gregorioccc79542011-02-19 00:05:26 -05001273 if http is None:
1274 http = httplib2.Http()
Joe Gregorio8b4c1732011-12-06 11:28:29 -05001275
JacobMoshenko8e905102011-06-20 09:53:10 -04001276 resp, content = http.request(self.token_uri, method='POST', body=body,
1277 headers=headers)
Joe Gregorioddb969a2012-07-11 11:04:12 -04001278 d = _parse_exchange_token_response(content)
1279 if resp.status == 200 and 'access_token' in d:
Joe Gregorio695fdc12011-01-16 16:46:55 -05001280 access_token = d['access_token']
1281 refresh_token = d.get('refresh_token', None)
1282 token_expiry = None
1283 if 'expires_in' in d:
Joe Gregorio562b7312011-09-15 09:06:38 -04001284 token_expiry = datetime.datetime.utcnow() + datetime.timedelta(
JacobMoshenko8e905102011-06-20 09:53:10 -04001285 seconds=int(d['expires_in']))
Joe Gregorio695fdc12011-01-16 16:46:55 -05001286
Joe Gregorio8b4c1732011-12-06 11:28:29 -05001287 if 'id_token' in d:
1288 d['id_token'] = _extract_id_token(d['id_token'])
1289
Joe Gregorio6ceea2d2012-08-24 11:57:58 -04001290 logger.info('Successfully retrieved access token')
JacobMoshenko8e905102011-06-20 09:53:10 -04001291 return OAuth2Credentials(access_token, self.client_id,
1292 self.client_secret, refresh_token, token_expiry,
Joe Gregorio8b4c1732011-12-06 11:28:29 -05001293 self.token_uri, self.user_agent,
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -08001294 revoke_uri=self.revoke_uri,
Joe Gregorio8b4c1732011-12-06 11:28:29 -05001295 id_token=d.get('id_token', None))
Joe Gregorio695fdc12011-01-16 16:46:55 -05001296 else:
Joe Gregorioe78621a2012-03-09 15:47:23 -05001297 logger.info('Failed to retrieve access token: %s' % content)
Joe Gregorioddb969a2012-07-11 11:04:12 -04001298 if 'error' in d:
1299 # you never know what those providers got to say
1300 error_msg = unicode(d['error'])
1301 else:
1302 error_msg = 'Invalid response: %s.' % str(resp.status)
Joe Gregorioccc79542011-02-19 00:05:26 -05001303 raise FlowExchangeError(error_msg)
Joe Gregoriof08a4982011-10-07 13:11:16 -04001304
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001305
1306@util.positional(2)
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -08001307def flow_from_clientsecrets(filename, scope, redirect_uri=None,
1308 message=None, cache=None):
Joe Gregoriof08a4982011-10-07 13:11:16 -04001309 """Create a Flow from a clientsecrets file.
1310
1311 Will create the right kind of Flow based on the contents of the clientsecrets
1312 file or will raise InvalidClientSecretsError for unknown types of Flows.
1313
1314 Args:
1315 filename: string, File name of client secrets.
Joe Gregorio5cf5d122012-11-16 16:36:12 -05001316 scope: string or iterable of strings, scope(s) to request.
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001317 redirect_uri: string, Either the string 'urn:ietf:wg:oauth:2.0:oob' for
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -08001318 a non-web-based application, or a URI that handles the callback from
1319 the authorization server.
Joe Gregoriof08a4982011-10-07 13:11:16 -04001320 message: string, A friendly string to display to the user if the
1321 clientsecrets file is missing or invalid. If message is provided then
1322 sys.exit will be called in the case of an error. If message in not
1323 provided then clientsecrets.InvalidClientSecretsError will be raised.
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001324 cache: An optional cache service client that implements get() and set()
Joe Gregorioc29aaa92012-07-16 16:16:31 -04001325 methods. See clientsecrets.loadfile() for details.
Joe Gregoriof08a4982011-10-07 13:11:16 -04001326
1327 Returns:
1328 A Flow object.
1329
1330 Raises:
1331 UnknownClientSecretsFlowError if the file describes an unknown kind of Flow.
1332 clientsecrets.InvalidClientSecretsError if the clientsecrets file is
1333 invalid.
1334 """
Joe Gregorio0984ef22011-10-14 13:17:43 -04001335 try:
Joe Gregorioc29aaa92012-07-16 16:16:31 -04001336 client_type, client_info = clientsecrets.loadfile(filename, cache=cache)
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -08001337 if client_type in (clientsecrets.TYPE_WEB, clientsecrets.TYPE_INSTALLED):
1338 constructor_kwargs = {
1339 'redirect_uri': redirect_uri,
1340 'auth_uri': client_info['auth_uri'],
1341 'token_uri': client_info['token_uri'],
1342 }
1343 revoke_uri = client_info.get('revoke_uri')
1344 if revoke_uri is not None:
1345 constructor_kwargs['revoke_uri'] = revoke_uri
1346 return OAuth2WebServerFlow(
1347 client_info['client_id'], client_info['client_secret'],
1348 scope, **constructor_kwargs)
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001349
Joe Gregorio0984ef22011-10-14 13:17:43 -04001350 except clientsecrets.InvalidClientSecretsError:
1351 if message:
1352 sys.exit(message)
1353 else:
1354 raise
Joe Gregoriof08a4982011-10-07 13:11:16 -04001355 else:
1356 raise UnknownClientSecretsFlowError(
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -08001357 'This OAuth 2.0 flow is unsupported: %r' % client_type)