blob: 1b0f828e5717cca8b770a6ff1004a523651263c9 [file] [log] [blame]
Joe Gregorio20a5aa92011-04-01 17:44:25 -04001# Copyright (C) 2010 Google Inc.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
Joe Gregorio695fdc12011-01-16 16:46:55 -050014
Joe Gregorio9da2ad82011-09-11 14:04:44 -040015"""An OAuth 2.0 client.
Joe Gregorio695fdc12011-01-16 16:46:55 -050016
Joe Gregorio9da2ad82011-09-11 14:04:44 -040017Tools for interacting with OAuth 2.0 protected resources.
Joe Gregorio695fdc12011-01-16 16:46:55 -050018"""
19
20__author__ = 'jcgregorio@google.com (Joe Gregorio)'
21
Joe Gregorio8b4c1732011-12-06 11:28:29 -050022import base64
Joe Gregoriof08a4982011-10-07 13:11:16 -040023import clientsecrets
Joe Gregorio695fdc12011-01-16 16:46:55 -050024import copy
25import datetime
26import httplib2
27import logging
Joe Gregorio8b4c1732011-12-06 11:28:29 -050028import os
Joe Gregoriof08a4982011-10-07 13:11:16 -040029import sys
Joe Gregorio8b4c1732011-12-06 11:28:29 -050030import time
Joe Gregorio695fdc12011-01-16 16:46:55 -050031import urllib
32import urlparse
33
Joe Gregorio68a8cfe2012-08-03 16:17:40 -040034from oauth2client import util
35from oauth2client.anyjson import simplejson
Joe Gregorio8b4c1732011-12-06 11:28:29 -050036
37HAS_OPENSSL = False
38try:
39 from oauth2client.crypt import Signer
40 from oauth2client.crypt import make_signed_jwt
41 from oauth2client.crypt import verify_signed_jwt_with_certs
42 HAS_OPENSSL = True
43except ImportError:
44 pass
45
Joe Gregorio695fdc12011-01-16 16:46:55 -050046try:
Joe Gregorio9da2ad82011-09-11 14:04:44 -040047 from urlparse import parse_qsl
Joe Gregorio695fdc12011-01-16 16:46:55 -050048except ImportError:
Joe Gregorio9da2ad82011-09-11 14:04:44 -040049 from cgi import parse_qsl
50
51logger = logging.getLogger(__name__)
Joe Gregorio695fdc12011-01-16 16:46:55 -050052
Joe Gregorio562b7312011-09-15 09:06:38 -040053# Expiry is stored in RFC3339 UTC format
Joe Gregorio8b4c1732011-12-06 11:28:29 -050054EXPIRY_FORMAT = '%Y-%m-%dT%H:%M:%SZ'
55
56# Which certs to use to validate id_tokens received.
57ID_TOKEN_VERIFICATON_CERTS = 'https://www.googleapis.com/oauth2/v1/certs'
Joe Gregorio562b7312011-09-15 09:06:38 -040058
Joe Gregoriof2326c02012-02-09 12:18:44 -050059# Constant to use for the out of band OAuth 2.0 flow.
60OOB_CALLBACK_URN = 'urn:ietf:wg:oauth:2.0:oob'
61
Joe Gregorio695fdc12011-01-16 16:46:55 -050062
63class Error(Exception):
64 """Base error for this module."""
65 pass
66
67
Joe Gregorioccc79542011-02-19 00:05:26 -050068class FlowExchangeError(Error):
Joe Gregorioca876e42011-02-22 19:39:42 -050069 """Error trying to exchange an authorization grant for an access token."""
Joe Gregorioccc79542011-02-19 00:05:26 -050070 pass
71
72
73class AccessTokenRefreshError(Error):
Joe Gregorioca876e42011-02-22 19:39:42 -050074 """Error trying to refresh an expired access token."""
Joe Gregorio695fdc12011-01-16 16:46:55 -050075 pass
76
Joe Gregoriof08a4982011-10-07 13:11:16 -040077class UnknownClientSecretsFlowError(Error):
78 """The client secrets file called for an unknown type of OAuth 2.0 flow. """
79 pass
80
Joe Gregorio695fdc12011-01-16 16:46:55 -050081
Joe Gregorio3b79fa82011-02-17 11:47:17 -050082class AccessTokenCredentialsError(Error):
83 """Having only the access_token means no refresh is possible."""
Joe Gregorio695fdc12011-01-16 16:46:55 -050084 pass
85
86
Joe Gregorio8b4c1732011-12-06 11:28:29 -050087class VerifyJwtTokenError(Error):
88 """Could on retrieve certificates for validation."""
89 pass
90
91
Joe Gregorio83f2ee62012-12-06 15:25:54 -050092class NonAsciiHeaderError(Error):
93 """Header names and values must be ASCII strings."""
94 pass
95
96
Joe Gregorio695fdc12011-01-16 16:46:55 -050097def _abstract():
98 raise NotImplementedError('You need to override this function')
99
100
Joe Gregorio9f2f38f2012-02-06 12:53:00 -0500101class MemoryCache(object):
102 """httplib2 Cache implementation which only caches locally."""
103
104 def __init__(self):
105 self.cache = {}
106
107 def get(self, key):
108 return self.cache.get(key)
109
110 def set(self, key, value):
111 self.cache[key] = value
112
113 def delete(self, key):
114 self.cache.pop(key, None)
115
116
Joe Gregorio695fdc12011-01-16 16:46:55 -0500117class Credentials(object):
118 """Base class for all Credentials objects.
119
Joe Gregorio562b7312011-09-15 09:06:38 -0400120 Subclasses must define an authorize() method that applies the credentials to
121 an HTTP transport.
122
123 Subclasses must also specify a classmethod named 'from_json' that takes a JSON
Joe Gregoriofa8cd9f2012-02-23 14:00:40 -0500124 string as input and returns an instaniated Credentials object.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500125 """
126
Joe Gregorio562b7312011-09-15 09:06:38 -0400127 NON_SERIALIZED_MEMBERS = ['store']
128
Joe Gregorio695fdc12011-01-16 16:46:55 -0500129 def authorize(self, http):
130 """Take an httplib2.Http instance (or equivalent) and
131 authorizes it for the set of credentials, usually by
132 replacing http.request() with a method that adds in
133 the appropriate headers and then delegates to the original
134 Http.request() method.
135 """
136 _abstract()
137
Joe Gregorio654f4a22012-02-09 14:15:44 -0500138 def refresh(self, http):
139 """Forces a refresh of the access_token.
140
141 Args:
142 http: httplib2.Http, an http object to be used to make the refresh
143 request.
144 """
145 _abstract()
146
147 def apply(self, headers):
148 """Add the authorization to the headers.
149
150 Args:
151 headers: dict, the headers to add the Authorization header to.
152 """
153 _abstract()
154
Joe Gregorio562b7312011-09-15 09:06:38 -0400155 def _to_json(self, strip):
156 """Utility function for creating a JSON representation of an instance of Credentials.
157
158 Args:
159 strip: array, An array of names of members to not include in the JSON.
160
161 Returns:
162 string, a JSON representation of this instance, suitable to pass to
163 from_json().
164 """
165 t = type(self)
166 d = copy.copy(self.__dict__)
167 for member in strip:
Joe Gregorio08cdcb82012-03-14 00:09:33 -0400168 if member in d:
169 del d[member]
Joe Gregorio562b7312011-09-15 09:06:38 -0400170 if 'token_expiry' in d and isinstance(d['token_expiry'], datetime.datetime):
171 d['token_expiry'] = d['token_expiry'].strftime(EXPIRY_FORMAT)
172 # Add in information we will need later to reconsistitue this instance.
173 d['_class'] = t.__name__
174 d['_module'] = t.__module__
175 return simplejson.dumps(d)
176
177 def to_json(self):
178 """Creating a JSON representation of an instance of Credentials.
179
180 Returns:
181 string, a JSON representation of this instance, suitable to pass to
182 from_json().
183 """
184 return self._to_json(Credentials.NON_SERIALIZED_MEMBERS)
185
186 @classmethod
187 def new_from_json(cls, s):
188 """Utility class method to instantiate a Credentials subclass from a JSON
189 representation produced by to_json().
190
191 Args:
192 s: string, JSON from to_json().
193
194 Returns:
195 An instance of the subclass of Credentials that was serialized with
196 to_json().
197 """
198 data = simplejson.loads(s)
199 # Find and call the right classmethod from_json() to restore the object.
200 module = data['_module']
Joe Gregoriofa8cd9f2012-02-23 14:00:40 -0500201 try:
202 m = __import__(module)
203 except ImportError:
204 # In case there's an object from the old package structure, update it
205 module = module.replace('.apiclient', '')
206 m = __import__(module)
207
Joe Gregorioe9b40f12011-10-13 10:03:28 -0400208 m = __import__(module, fromlist=module.split('.')[:-1])
Joe Gregorio562b7312011-09-15 09:06:38 -0400209 kls = getattr(m, data['_class'])
210 from_json = getattr(kls, 'from_json')
211 return from_json(s)
212
Joe Gregorio08cdcb82012-03-14 00:09:33 -0400213 @classmethod
214 def from_json(cls, s):
Joe Gregorio401b8422012-05-03 16:35:35 -0400215 """Instantiate a Credentials object from a JSON description of it.
216
217 The JSON should have been produced by calling .to_json() on the object.
Joe Gregorio08cdcb82012-03-14 00:09:33 -0400218
219 Args:
220 data: dict, A deserialized JSON object.
221
222 Returns:
223 An instance of a Credentials subclass.
224 """
225 return Credentials()
226
JacobMoshenko8e905102011-06-20 09:53:10 -0400227
Joe Gregorio695fdc12011-01-16 16:46:55 -0500228class Flow(object):
229 """Base class for all Flow objects."""
230 pass
231
232
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500233class Storage(object):
234 """Base class for all Storage objects.
235
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400236 Store and retrieve a single credential. This class supports locking
237 such that multiple processes and threads can operate on a single
238 store.
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500239 """
240
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400241 def acquire_lock(self):
242 """Acquires any lock necessary to access this Storage.
243
Joe Gregorio401b8422012-05-03 16:35:35 -0400244 This lock is not reentrant.
245 """
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400246 pass
247
248 def release_lock(self):
249 """Release the Storage lock.
250
251 Trying to release a lock that isn't held will result in a
252 RuntimeError.
253 """
254 pass
255
256 def locked_get(self):
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500257 """Retrieve credential.
258
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400259 The Storage lock must be held when this is called.
260
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500261 Returns:
Joe Gregorio06d852b2011-03-25 15:03:10 -0400262 oauth2client.client.Credentials
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500263 """
264 _abstract()
265
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400266 def locked_put(self, credentials):
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500267 """Write a credential.
268
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400269 The Storage lock must be held when this is called.
270
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500271 Args:
272 credentials: Credentials, the credentials to store.
273 """
274 _abstract()
275
Joe Gregorioec75dc12012-02-06 13:40:42 -0500276 def locked_delete(self):
277 """Delete a credential.
278
279 The Storage lock must be held when this is called.
280 """
281 _abstract()
282
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400283 def get(self):
284 """Retrieve credential.
285
286 The Storage lock must *not* be held when this is called.
287
288 Returns:
289 oauth2client.client.Credentials
290 """
291 self.acquire_lock()
292 try:
293 return self.locked_get()
294 finally:
295 self.release_lock()
296
297 def put(self, credentials):
298 """Write a credential.
299
300 The Storage lock must be held when this is called.
301
302 Args:
303 credentials: Credentials, the credentials to store.
304 """
305 self.acquire_lock()
306 try:
307 self.locked_put(credentials)
308 finally:
309 self.release_lock()
310
Joe Gregorioec75dc12012-02-06 13:40:42 -0500311 def delete(self):
312 """Delete credential.
313
314 Frees any resources associated with storing the credential.
315 The Storage lock must *not* be held when this is called.
316
317 Returns:
318 None
319 """
320 self.acquire_lock()
321 try:
322 return self.locked_delete()
323 finally:
324 self.release_lock()
325
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500326
Joe Gregorio83f2ee62012-12-06 15:25:54 -0500327def clean_headers(headers):
328 """Forces header keys and values to be strings, i.e not unicode.
329
330 The httplib module just concats the header keys and values in a way that may
331 make the message header a unicode string, which, if it then tries to
332 contatenate to a binary request body may result in a unicode decode error.
333
334 Args:
335 headers: dict, A dictionary of headers.
336
337 Returns:
338 The same dictionary but with all the keys converted to strings.
339 """
340 clean = {}
341 try:
342 for k, v in headers.iteritems():
343 clean[str(k)] = str(v)
344 except UnicodeEncodeError:
345 raise NonAsciiHeaderError(k + ': ' + v)
346 return clean
347
348
Joe Gregorio695fdc12011-01-16 16:46:55 -0500349class OAuth2Credentials(Credentials):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400350 """Credentials object for OAuth 2.0.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500351
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500352 Credentials can be applied to an httplib2.Http object using the authorize()
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500353 method, which then adds the OAuth 2.0 access token to each request.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500354
355 OAuth2Credentials objects may be safely pickled and unpickled.
356 """
357
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400358 @util.positional(8)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500359 def __init__(self, access_token, client_id, client_secret, refresh_token,
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500360 token_expiry, token_uri, user_agent, id_token=None):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400361 """Create an instance of OAuth2Credentials.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500362
363 This constructor is not usually called by the user, instead
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500364 OAuth2Credentials objects are instantiated by the OAuth2WebServerFlow.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500365
366 Args:
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400367 access_token: string, access token.
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500368 client_id: string, client identifier.
369 client_secret: string, client secret.
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500370 refresh_token: string, refresh token.
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400371 token_expiry: datetime, when the access_token expires.
372 token_uri: string, URI of token endpoint.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500373 user_agent: string, The HTTP User-Agent to provide for this application.
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500374 id_token: object, The identity of the resource owner.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500375
Joe Gregorio695fdc12011-01-16 16:46:55 -0500376 Notes:
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500377 store: callable, A callable that when passed a Credential
Joe Gregorio695fdc12011-01-16 16:46:55 -0500378 will store the credential back to where it came from.
379 This is needed to store the latest access_token if it
380 has expired and been refreshed.
381 """
382 self.access_token = access_token
383 self.client_id = client_id
384 self.client_secret = client_secret
385 self.refresh_token = refresh_token
386 self.store = None
387 self.token_expiry = token_expiry
388 self.token_uri = token_uri
389 self.user_agent = user_agent
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500390 self.id_token = id_token
Joe Gregorio695fdc12011-01-16 16:46:55 -0500391
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500392 # True if the credentials have been revoked or expired and can't be
393 # refreshed.
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400394 self.invalid = False
Joe Gregorio9ce4b622011-02-17 15:32:11 -0500395
Joe Gregorio654f4a22012-02-09 14:15:44 -0500396 def authorize(self, http):
397 """Authorize an httplib2.Http instance with these credentials.
398
399 The modified http.request method will add authentication headers to each
400 request and will refresh access_tokens when a 401 is received on a
401 request. In addition the http.request method has a credentials property,
402 http.request.credentials, which is the Credentials object that authorized
403 it.
404
405 Args:
406 http: An instance of httplib2.Http
407 or something that acts like it.
408
409 Returns:
410 A modified instance of http that was passed in.
411
412 Example:
413
414 h = httplib2.Http()
415 h = credentials.authorize(h)
416
417 You can't create a new OAuth subclass of httplib2.Authenication
418 because it never gets passed the absolute URI, which is needed for
419 signing. So instead we have to overload 'request' with a closure
420 that adds in the Authorization header and then calls the original
421 version of 'request()'.
422 """
423 request_orig = http.request
424
425 # The closure that will replace 'httplib2.Http.request'.
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400426 @util.positional(1)
Joe Gregorio654f4a22012-02-09 14:15:44 -0500427 def new_request(uri, method='GET', body=None, headers=None,
428 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
429 connection_type=None):
430 if not self.access_token:
431 logger.info('Attempting refresh to obtain initial access_token')
432 self._refresh(request_orig)
433
434 # Modify the request headers to add the appropriate
435 # Authorization header.
436 if headers is None:
437 headers = {}
438 self.apply(headers)
439
440 if self.user_agent is not None:
441 if 'user-agent' in headers:
442 headers['user-agent'] = self.user_agent + ' ' + headers['user-agent']
443 else:
444 headers['user-agent'] = self.user_agent
445
Joe Gregorio83f2ee62012-12-06 15:25:54 -0500446 resp, content = request_orig(uri, method, body, clean_headers(headers),
Joe Gregorio654f4a22012-02-09 14:15:44 -0500447 redirections, connection_type)
448
Joe Gregorio7c7c6b12012-07-16 16:31:01 -0400449 # Older API (GData) respond with 403
450 if resp.status in [401, 403]:
451 logger.info('Refreshing due to a %s' % str(resp.status))
Joe Gregorio654f4a22012-02-09 14:15:44 -0500452 self._refresh(request_orig)
453 self.apply(headers)
Joe Gregorio83f2ee62012-12-06 15:25:54 -0500454 return request_orig(uri, method, body, clean_headers(headers),
Joe Gregorio654f4a22012-02-09 14:15:44 -0500455 redirections, connection_type)
456 else:
457 return (resp, content)
458
459 # Replace the request method with our own closure.
460 http.request = new_request
461
462 # Set credentials as a property of the request method.
463 setattr(http.request, 'credentials', self)
464
465 return http
466
467 def refresh(self, http):
468 """Forces a refresh of the access_token.
469
470 Args:
471 http: httplib2.Http, an http object to be used to make the refresh
472 request.
473 """
474 self._refresh(http.request)
475
476 def apply(self, headers):
477 """Add the authorization to the headers.
478
479 Args:
480 headers: dict, the headers to add the Authorization header to.
481 """
482 headers['Authorization'] = 'Bearer ' + self.access_token
483
Joe Gregorio562b7312011-09-15 09:06:38 -0400484 def to_json(self):
485 return self._to_json(Credentials.NON_SERIALIZED_MEMBERS)
486
487 @classmethod
488 def from_json(cls, s):
489 """Instantiate a Credentials object from a JSON description of it. The JSON
490 should have been produced by calling .to_json() on the object.
491
492 Args:
493 data: dict, A deserialized JSON object.
494
495 Returns:
496 An instance of a Credentials subclass.
497 """
498 data = simplejson.loads(s)
499 if 'token_expiry' in data and not isinstance(data['token_expiry'],
500 datetime.datetime):
Joe Gregorio1daa71b2011-09-15 18:12:14 -0400501 try:
502 data['token_expiry'] = datetime.datetime.strptime(
503 data['token_expiry'], EXPIRY_FORMAT)
504 except:
505 data['token_expiry'] = None
Joe Gregorio562b7312011-09-15 09:06:38 -0400506 retval = OAuth2Credentials(
507 data['access_token'],
508 data['client_id'],
509 data['client_secret'],
510 data['refresh_token'],
511 data['token_expiry'],
512 data['token_uri'],
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500513 data['user_agent'],
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400514 id_token=data.get('id_token', None))
Joe Gregorio562b7312011-09-15 09:06:38 -0400515 retval.invalid = data['invalid']
516 return retval
517
Joe Gregorio9ce4b622011-02-17 15:32:11 -0500518 @property
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400519 def access_token_expired(self):
520 """True if the credential is expired or invalid.
521
522 If the token_expiry isn't set, we assume the token doesn't expire.
523 """
524 if self.invalid:
525 return True
526
527 if not self.token_expiry:
528 return False
529
Joe Gregorio562b7312011-09-15 09:06:38 -0400530 now = datetime.datetime.utcnow()
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400531 if now >= self.token_expiry:
532 logger.info('access_token is expired. Now: %s, token_expiry: %s',
533 now, self.token_expiry)
534 return True
535 return False
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500536
Joe Gregorio695fdc12011-01-16 16:46:55 -0500537 def set_store(self, store):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400538 """Set the Storage for the credential.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500539
540 Args:
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400541 store: Storage, an implementation of Stroage object.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500542 This is needed to store the latest access_token if it
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400543 has expired and been refreshed. This implementation uses
544 locking to check for updates before updating the
545 access_token.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500546 """
547 self.store = store
548
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400549 def _updateFromCredential(self, other):
550 """Update this Credential from another instance."""
551 self.__dict__.update(other.__getstate__())
552
Joe Gregorio695fdc12011-01-16 16:46:55 -0500553 def __getstate__(self):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400554 """Trim the state down to something that can be pickled."""
Joe Gregorio695fdc12011-01-16 16:46:55 -0500555 d = copy.copy(self.__dict__)
556 del d['store']
557 return d
558
559 def __setstate__(self, state):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400560 """Reconstitute the state of the object from being pickled."""
Joe Gregorio695fdc12011-01-16 16:46:55 -0500561 self.__dict__.update(state)
562 self.store = None
563
JacobMoshenko8e905102011-06-20 09:53:10 -0400564 def _generate_refresh_request_body(self):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400565 """Generate the body that will be used in the refresh request."""
JacobMoshenko8e905102011-06-20 09:53:10 -0400566 body = urllib.urlencode({
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400567 'grant_type': 'refresh_token',
568 'client_id': self.client_id,
569 'client_secret': self.client_secret,
570 'refresh_token': self.refresh_token,
571 })
JacobMoshenko8e905102011-06-20 09:53:10 -0400572 return body
573
574 def _generate_refresh_request_headers(self):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400575 """Generate the headers that will be used in the refresh request."""
JacobMoshenko8e905102011-06-20 09:53:10 -0400576 headers = {
JacobMoshenko8e905102011-06-20 09:53:10 -0400577 'content-type': 'application/x-www-form-urlencoded',
578 }
JacobMoshenkocb6d8912011-07-08 13:35:15 -0400579
580 if self.user_agent is not None:
581 headers['user-agent'] = self.user_agent
582
JacobMoshenko8e905102011-06-20 09:53:10 -0400583 return headers
584
Joe Gregorio695fdc12011-01-16 16:46:55 -0500585 def _refresh(self, http_request):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400586 """Refreshes the access_token.
587
588 This method first checks by reading the Storage object if available.
589 If a refresh is still needed, it holds the Storage lock until the
590 refresh is completed.
Joe Gregorio654f4a22012-02-09 14:15:44 -0500591
592 Args:
593 http_request: callable, a callable that matches the method signature of
594 httplib2.Http.request, used to make the refresh request.
595
596 Raises:
597 AccessTokenRefreshError: When the refresh fails.
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400598 """
599 if not self.store:
600 self._do_refresh_request(http_request)
601 else:
602 self.store.acquire_lock()
603 try:
604 new_cred = self.store.locked_get()
605 if (new_cred and not new_cred.invalid and
606 new_cred.access_token != self.access_token):
607 logger.info('Updated access_token read from Storage')
608 self._updateFromCredential(new_cred)
609 else:
610 self._do_refresh_request(http_request)
611 finally:
612 self.store.release_lock()
613
614 def _do_refresh_request(self, http_request):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500615 """Refresh the access_token using the refresh_token.
616
617 Args:
Joe Gregorio654f4a22012-02-09 14:15:44 -0500618 http_request: callable, a callable that matches the method signature of
619 httplib2.Http.request, used to make the refresh request.
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400620
621 Raises:
622 AccessTokenRefreshError: When the refresh fails.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500623 """
JacobMoshenko8e905102011-06-20 09:53:10 -0400624 body = self._generate_refresh_request_body()
625 headers = self._generate_refresh_request_headers()
626
Joe Gregorio4b4002f2012-06-14 15:41:01 -0400627 logger.info('Refreshing access_token')
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500628 resp, content = http_request(
629 self.token_uri, method='POST', body=body, headers=headers)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500630 if resp.status == 200:
631 # TODO(jcgregorio) Raise an error if loads fails?
632 d = simplejson.loads(content)
633 self.access_token = d['access_token']
634 self.refresh_token = d.get('refresh_token', self.refresh_token)
635 if 'expires_in' in d:
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500636 self.token_expiry = datetime.timedelta(
Joe Gregorio562b7312011-09-15 09:06:38 -0400637 seconds=int(d['expires_in'])) + datetime.datetime.utcnow()
Joe Gregorio695fdc12011-01-16 16:46:55 -0500638 else:
639 self.token_expiry = None
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400640 if self.store:
641 self.store.locked_put(self)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500642 else:
JacobMoshenko8e905102011-06-20 09:53:10 -0400643 # An {'error':...} response body means the token is expired or revoked,
644 # so we flag the credentials as such.
Joe Gregorioe78621a2012-03-09 15:47:23 -0500645 logger.info('Failed to retrieve access token: %s' % content)
Joe Gregorioccc79542011-02-19 00:05:26 -0500646 error_msg = 'Invalid response %s.' % resp['status']
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500647 try:
648 d = simplejson.loads(content)
649 if 'error' in d:
Joe Gregorioccc79542011-02-19 00:05:26 -0500650 error_msg = d['error']
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400651 self.invalid = True
652 if self.store:
653 self.store.locked_put(self)
Joe Gregoriofd08e432012-08-09 14:17:41 -0400654 except StandardError:
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500655 pass
Joe Gregorioccc79542011-02-19 00:05:26 -0500656 raise AccessTokenRefreshError(error_msg)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500657
Joe Gregorio695fdc12011-01-16 16:46:55 -0500658
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500659class AccessTokenCredentials(OAuth2Credentials):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400660 """Credentials object for OAuth 2.0.
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500661
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400662 Credentials can be applied to an httplib2.Http object using the
663 authorize() method, which then signs each request from that object
664 with the OAuth 2.0 access token. This set of credentials is for the
665 use case where you have acquired an OAuth 2.0 access_token from
666 another place such as a JavaScript client or another web
667 application, and wish to use it from Python. Because only the
668 access_token is present it can not be refreshed and will in time
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500669 expire.
670
Joe Gregorio9ce4b622011-02-17 15:32:11 -0500671 AccessTokenCredentials objects may be safely pickled and unpickled.
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500672
673 Usage:
674 credentials = AccessTokenCredentials('<an access token>',
675 'my-user-agent/1.0')
676 http = httplib2.Http()
677 http = credentials.authorize(http)
678
679 Exceptions:
680 AccessTokenCredentialsExpired: raised when the access_token expires or is
681 revoked.
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500682 """
683
684 def __init__(self, access_token, user_agent):
685 """Create an instance of OAuth2Credentials
686
687 This is one of the few types if Credentials that you should contrust,
688 Credentials objects are usually instantiated by a Flow.
689
690 Args:
ade@google.com93a7f7c2011-02-23 16:00:37 +0000691 access_token: string, access token.
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500692 user_agent: string, The HTTP User-Agent to provide for this application.
693
694 Notes:
695 store: callable, a callable that when passed a Credential
696 will store the credential back to where it came from.
697 """
698 super(AccessTokenCredentials, self).__init__(
699 access_token,
700 None,
701 None,
702 None,
703 None,
704 None,
705 user_agent)
706
Joe Gregorio562b7312011-09-15 09:06:38 -0400707
708 @classmethod
709 def from_json(cls, s):
710 data = simplejson.loads(s)
711 retval = AccessTokenCredentials(
712 data['access_token'],
713 data['user_agent'])
714 return retval
715
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500716 def _refresh(self, http_request):
717 raise AccessTokenCredentialsError(
718 "The access_token is expired or invalid and can't be refreshed.")
719
JacobMoshenko8e905102011-06-20 09:53:10 -0400720
721class AssertionCredentials(OAuth2Credentials):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400722 """Abstract Credentials object used for OAuth 2.0 assertion grants.
JacobMoshenko8e905102011-06-20 09:53:10 -0400723
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400724 This credential does not require a flow to instantiate because it
725 represents a two legged flow, and therefore has all of the required
726 information to generate and refresh its own access tokens. It must
727 be subclassed to generate the appropriate assertion string.
JacobMoshenko8e905102011-06-20 09:53:10 -0400728
729 AssertionCredentials objects may be safely pickled and unpickled.
730 """
731
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400732 @util.positional(2)
733 def __init__(self, assertion_type, user_agent=None,
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400734 token_uri='https://accounts.google.com/o/oauth2/token',
735 **unused_kwargs):
736 """Constructor for AssertionFlowCredentials.
JacobMoshenko8e905102011-06-20 09:53:10 -0400737
738 Args:
739 assertion_type: string, assertion type that will be declared to the auth
740 server
741 user_agent: string, The HTTP User-Agent to provide for this application.
742 token_uri: string, URI for token endpoint. For convenience
743 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
744 """
745 super(AssertionCredentials, self).__init__(
746 None,
747 None,
748 None,
749 None,
750 None,
751 token_uri,
752 user_agent)
753 self.assertion_type = assertion_type
754
755 def _generate_refresh_request_body(self):
756 assertion = self._generate_assertion()
757
758 body = urllib.urlencode({
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400759 'assertion_type': self.assertion_type,
760 'assertion': assertion,
761 'grant_type': 'assertion',
762 })
JacobMoshenko8e905102011-06-20 09:53:10 -0400763
764 return body
765
766 def _generate_assertion(self):
767 """Generate the assertion string that will be used in the access token
768 request.
769 """
770 _abstract()
771
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500772if HAS_OPENSSL:
773 # PyOpenSSL is not a prerequisite for oauth2client, so if it is missing then
774 # don't create the SignedJwtAssertionCredentials or the verify_id_token()
775 # method.
776
777 class SignedJwtAssertionCredentials(AssertionCredentials):
778 """Credentials object used for OAuth 2.0 Signed JWT assertion grants.
779
Joe Gregorio672051e2012-07-10 09:11:45 -0400780 This credential does not require a flow to instantiate because it represents
781 a two legged flow, and therefore has all of the required information to
782 generate and refresh its own access tokens.
783
784 SignedJwtAssertionCredentials requires PyOpenSSL and because of that it does
785 not work on App Engine. For App Engine you may consider using
786 AppAssertionCredentials.
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500787 """
788
789 MAX_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds
790
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400791 @util.positional(4)
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500792 def __init__(self,
793 service_account_name,
794 private_key,
795 scope,
796 private_key_password='notasecret',
797 user_agent=None,
798 token_uri='https://accounts.google.com/o/oauth2/token',
799 **kwargs):
800 """Constructor for SignedJwtAssertionCredentials.
801
802 Args:
803 service_account_name: string, id for account, usually an email address.
804 private_key: string, private key in P12 format.
Joe Gregorio5cf5d122012-11-16 16:36:12 -0500805 scope: string or iterable of strings, scope(s) of the credentials being
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500806 requested.
807 private_key_password: string, password for private_key.
808 user_agent: string, HTTP User-Agent to provide for this application.
809 token_uri: string, URI for token endpoint. For convenience
810 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
811 kwargs: kwargs, Additional parameters to add to the JWT token, for
812 example prn=joe@xample.org."""
813
814 super(SignedJwtAssertionCredentials, self).__init__(
815 'http://oauth.net/grant_type/jwt/1.0/bearer',
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400816 user_agent=user_agent,
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500817 token_uri=token_uri,
818 )
819
Joe Gregorio5cf5d122012-11-16 16:36:12 -0500820 self.scope = util.scopes_to_string(scope)
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500821
Joe Gregorioa19f3a72012-07-11 15:35:35 -0400822 # Keep base64 encoded so it can be stored in JSON.
823 self.private_key = base64.b64encode(private_key)
824
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500825 self.private_key_password = private_key_password
826 self.service_account_name = service_account_name
827 self.kwargs = kwargs
828
829 @classmethod
830 def from_json(cls, s):
831 data = simplejson.loads(s)
832 retval = SignedJwtAssertionCredentials(
833 data['service_account_name'],
Joe Gregorioa19f3a72012-07-11 15:35:35 -0400834 base64.b64decode(data['private_key']),
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500835 data['scope'],
Joe Gregorioa19f3a72012-07-11 15:35:35 -0400836 private_key_password=data['private_key_password'],
837 user_agent=data['user_agent'],
838 token_uri=data['token_uri'],
839 **data['kwargs']
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500840 )
841 retval.invalid = data['invalid']
Joe Gregorioa19f3a72012-07-11 15:35:35 -0400842 retval.access_token = data['access_token']
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500843 return retval
844
845 def _generate_assertion(self):
846 """Generate the assertion that will be used in the request."""
847 now = long(time.time())
848 payload = {
849 'aud': self.token_uri,
850 'scope': self.scope,
851 'iat': now,
852 'exp': now + SignedJwtAssertionCredentials.MAX_TOKEN_LIFETIME_SECS,
853 'iss': self.service_account_name
854 }
855 payload.update(self.kwargs)
Joe Gregorioe78621a2012-03-09 15:47:23 -0500856 logger.debug(str(payload))
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500857
Joe Gregorioa19f3a72012-07-11 15:35:35 -0400858 private_key = base64.b64decode(self.private_key)
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500859 return make_signed_jwt(
Joe Gregorioa19f3a72012-07-11 15:35:35 -0400860 Signer.from_string(private_key, self.private_key_password), payload)
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500861
Joe Gregorio9f2f38f2012-02-06 12:53:00 -0500862 # Only used in verify_id_token(), which is always calling to the same URI
863 # for the certs.
864 _cached_http = httplib2.Http(MemoryCache())
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500865
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400866 @util.positional(2)
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500867 def verify_id_token(id_token, audience, http=None,
868 cert_uri=ID_TOKEN_VERIFICATON_CERTS):
869 """Verifies a signed JWT id_token.
870
Joe Gregorio672051e2012-07-10 09:11:45 -0400871 This function requires PyOpenSSL and because of that it does not work on
872 App Engine. For App Engine you may consider using AppAssertionCredentials.
873
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500874 Args:
875 id_token: string, A Signed JWT.
876 audience: string, The audience 'aud' that the token should be for.
877 http: httplib2.Http, instance to use to make the HTTP request. Callers
878 should supply an instance that has caching enabled.
879 cert_uri: string, URI of the certificates in JSON format to
880 verify the JWT against.
881
882 Returns:
883 The deserialized JSON in the JWT.
884
885 Raises:
886 oauth2client.crypt.AppIdentityError if the JWT fails to verify.
887 """
888 if http is None:
Joe Gregorio9f2f38f2012-02-06 12:53:00 -0500889 http = _cached_http
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500890
891 resp, content = http.request(cert_uri)
892
893 if resp.status == 200:
894 certs = simplejson.loads(content)
895 return verify_signed_jwt_with_certs(id_token, certs, audience)
896 else:
897 raise VerifyJwtTokenError('Status code: %d' % resp.status)
898
899
900def _urlsafe_b64decode(b64string):
Joe Gregoriobd512b52011-12-06 15:39:26 -0500901 # Guard against unicode strings, which base64 can't handle.
902 b64string = b64string.encode('ascii')
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500903 padded = b64string + '=' * (4 - len(b64string) % 4)
904 return base64.urlsafe_b64decode(padded)
905
906
907def _extract_id_token(id_token):
908 """Extract the JSON payload from a JWT.
909
910 Does the extraction w/o checking the signature.
911
912 Args:
913 id_token: string, OAuth 2.0 id_token.
914
915 Returns:
916 object, The deserialized JSON payload.
917 """
918 segments = id_token.split('.')
919
920 if (len(segments) != 3):
921 raise VerifyJwtTokenError(
922 'Wrong number of segments in token: %s' % id_token)
923
924 return simplejson.loads(_urlsafe_b64decode(segments[1]))
925
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400926
Joe Gregorioddb969a2012-07-11 11:04:12 -0400927def _parse_exchange_token_response(content):
928 """Parses response of an exchange token request.
929
930 Most providers return JSON but some (e.g. Facebook) return a
931 url-encoded string.
932
933 Args:
934 content: The body of a response
935
936 Returns:
937 Content as a dictionary object. Note that the dict could be empty,
938 i.e. {}. That basically indicates a failure.
939 """
940 resp = {}
941 try:
942 resp = simplejson.loads(content)
943 except StandardError:
944 # different JSON libs raise different exceptions,
945 # so we just do a catch-all here
946 resp = dict(parse_qsl(content))
947
948 # some providers respond with 'expires', others with 'expires_in'
949 if resp and 'expires' in resp:
950 resp['expires_in'] = resp.pop('expires')
951
952 return resp
953
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400954
955@util.positional(4)
Joe Gregorio32d852d2012-06-14 09:08:18 -0400956def credentials_from_code(client_id, client_secret, scope, code,
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400957 redirect_uri='postmessage', http=None, user_agent=None,
958 token_uri='https://accounts.google.com/o/oauth2/token'):
Joe Gregorio32d852d2012-06-14 09:08:18 -0400959 """Exchanges an authorization code for an OAuth2Credentials object.
960
961 Args:
962 client_id: string, client identifier.
963 client_secret: string, client secret.
Joe Gregorio5cf5d122012-11-16 16:36:12 -0500964 scope: string or iterable of strings, scope(s) to request.
Joe Gregorio32d852d2012-06-14 09:08:18 -0400965 code: string, An authroization code, most likely passed down from
966 the client
967 redirect_uri: string, this is generally set to 'postmessage' to match the
968 redirect_uri that the client specified
969 http: httplib2.Http, optional http instance to use to do the fetch
970 token_uri: string, URI for token endpoint. For convenience
971 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
972 Returns:
973 An OAuth2Credentials object.
974
975 Raises:
976 FlowExchangeError if the authorization code cannot be exchanged for an
977 access token
978 """
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400979 flow = OAuth2WebServerFlow(client_id, client_secret, scope,
980 redirect_uri=redirect_uri, user_agent=user_agent,
981 auth_uri='https://accounts.google.com/o/oauth2/auth',
982 token_uri=token_uri)
Joe Gregorio32d852d2012-06-14 09:08:18 -0400983
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400984 credentials = flow.step2_exchange(code, http=http)
Joe Gregorio32d852d2012-06-14 09:08:18 -0400985 return credentials
986
987
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400988@util.positional(3)
Joe Gregorio32d852d2012-06-14 09:08:18 -0400989def credentials_from_clientsecrets_and_code(filename, scope, code,
990 message = None,
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400991 redirect_uri='postmessage',
Joe Gregorioc29aaa92012-07-16 16:16:31 -0400992 http=None,
993 cache=None):
Joe Gregorio32d852d2012-06-14 09:08:18 -0400994 """Returns OAuth2Credentials from a clientsecrets file and an auth code.
995
996 Will create the right kind of Flow based on the contents of the clientsecrets
997 file or will raise InvalidClientSecretsError for unknown types of Flows.
998
999 Args:
1000 filename: string, File name of clientsecrets.
Joe Gregorio5cf5d122012-11-16 16:36:12 -05001001 scope: string or iterable of strings, scope(s) to request.
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001002 code: string, An authorization code, most likely passed down from
Joe Gregorio32d852d2012-06-14 09:08:18 -04001003 the client
1004 message: string, A friendly string to display to the user if the
1005 clientsecrets file is missing or invalid. If message is provided then
1006 sys.exit will be called in the case of an error. If message in not
1007 provided then clientsecrets.InvalidClientSecretsError will be raised.
1008 redirect_uri: string, this is generally set to 'postmessage' to match the
1009 redirect_uri that the client specified
1010 http: httplib2.Http, optional http instance to use to do the fetch
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001011 cache: An optional cache service client that implements get() and set()
Joe Gregorioc29aaa92012-07-16 16:16:31 -04001012 methods. See clientsecrets.loadfile() for details.
Joe Gregorio32d852d2012-06-14 09:08:18 -04001013
1014 Returns:
1015 An OAuth2Credentials object.
1016
1017 Raises:
1018 FlowExchangeError if the authorization code cannot be exchanged for an
1019 access token
1020 UnknownClientSecretsFlowError if the file describes an unknown kind of Flow.
1021 clientsecrets.InvalidClientSecretsError if the clientsecrets file is
1022 invalid.
1023 """
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001024 flow = flow_from_clientsecrets(filename, scope, message=message, cache=cache,
1025 redirect_uri=redirect_uri)
1026 credentials = flow.step2_exchange(code, http=http)
Joe Gregorio32d852d2012-06-14 09:08:18 -04001027 return credentials
1028
JacobMoshenko8e905102011-06-20 09:53:10 -04001029
Joe Gregorio695fdc12011-01-16 16:46:55 -05001030class OAuth2WebServerFlow(Flow):
1031 """Does the Web Server Flow for OAuth 2.0.
1032
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001033 OAuth2WebServerFlow objects may be safely pickled and unpickled.
Joe Gregorio695fdc12011-01-16 16:46:55 -05001034 """
1035
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001036 @util.positional(4)
1037 def __init__(self, client_id, client_secret, scope,
1038 redirect_uri=None,
1039 user_agent=None,
Joe Gregorio9da2ad82011-09-11 14:04:44 -04001040 auth_uri='https://accounts.google.com/o/oauth2/auth',
1041 token_uri='https://accounts.google.com/o/oauth2/token',
1042 **kwargs):
1043 """Constructor for OAuth2WebServerFlow.
Joe Gregorio695fdc12011-01-16 16:46:55 -05001044
Joe Gregorio32f73192012-10-23 16:13:44 -04001045 The kwargs argument is used to set extra query parameters on the
1046 auth_uri. For example, the access_type and approval_prompt
1047 query parameters can be set via kwargs.
1048
Joe Gregorio695fdc12011-01-16 16:46:55 -05001049 Args:
Joe Gregorio7c22ab22011-02-16 15:32:39 -05001050 client_id: string, client identifier.
1051 client_secret: string client secret.
Joe Gregorio5cf5d122012-11-16 16:36:12 -05001052 scope: string or iterable of strings, scope(s) of the credentials being
Joe Gregoriof2f8a5a2011-10-14 15:11:29 -04001053 requested.
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001054 redirect_uri: string, Either the string 'urn:ietf:wg:oauth:2.0:oob' for
1055 a non-web-based application, or a URI that handles the callback from
1056 the authorization server.
Joe Gregorio695fdc12011-01-16 16:46:55 -05001057 user_agent: string, HTTP User-Agent to provide for this application.
Joe Gregorio7c22ab22011-02-16 15:32:39 -05001058 auth_uri: string, URI for authorization endpoint. For convenience
1059 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
1060 token_uri: string, URI for token endpoint. For convenience
1061 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
Joe Gregorio695fdc12011-01-16 16:46:55 -05001062 **kwargs: dict, The keyword arguments are all optional and required
1063 parameters for the OAuth calls.
1064 """
1065 self.client_id = client_id
1066 self.client_secret = client_secret
Joe Gregorio5cf5d122012-11-16 16:36:12 -05001067 self.scope = util.scopes_to_string(scope)
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001068 self.redirect_uri = redirect_uri
Joe Gregorio695fdc12011-01-16 16:46:55 -05001069 self.user_agent = user_agent
Joe Gregorio7c22ab22011-02-16 15:32:39 -05001070 self.auth_uri = auth_uri
Joe Gregorio695fdc12011-01-16 16:46:55 -05001071 self.token_uri = token_uri
Joe Gregorio69a0aca2011-11-03 10:47:32 -04001072 self.params = {
1073 'access_type': 'offline',
Joe Gregorio32f73192012-10-23 16:13:44 -04001074 'response_type': 'code',
Joe Gregorio69a0aca2011-11-03 10:47:32 -04001075 }
1076 self.params.update(kwargs)
Joe Gregorio695fdc12011-01-16 16:46:55 -05001077
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001078 @util.positional(1)
1079 def step1_get_authorize_url(self, redirect_uri=None):
Joe Gregorio695fdc12011-01-16 16:46:55 -05001080 """Returns a URI to redirect to the provider.
1081
1082 Args:
Joe Gregoriof2326c02012-02-09 12:18:44 -05001083 redirect_uri: string, Either the string 'urn:ietf:wg:oauth:2.0:oob' for
1084 a non-web-based application, or a URI that handles the callback from
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001085 the authorization server. This parameter is deprecated, please move to
1086 passing the redirect_uri in via the constructor.
Joe Gregorio695fdc12011-01-16 16:46:55 -05001087
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001088 Returns:
1089 A URI as a string to redirect the user to begin the authorization flow.
Joe Gregorio695fdc12011-01-16 16:46:55 -05001090 """
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001091 if redirect_uri is not None:
1092 logger.warning(('The redirect_uri parameter for'
1093 'OAuth2WebServerFlow.step1_get_authorize_url is deprecated. Please'
1094 'move to passing the redirect_uri in via the constructor.'))
1095 self.redirect_uri = redirect_uri
Joe Gregorio695fdc12011-01-16 16:46:55 -05001096
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001097 if self.redirect_uri is None:
1098 raise ValueError('The value of redirect_uri must not be None.')
1099
Joe Gregorio695fdc12011-01-16 16:46:55 -05001100 query = {
Joe Gregorio9da2ad82011-09-11 14:04:44 -04001101 'client_id': self.client_id,
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001102 'redirect_uri': self.redirect_uri,
Joe Gregorio9da2ad82011-09-11 14:04:44 -04001103 'scope': self.scope,
1104 }
Joe Gregorio695fdc12011-01-16 16:46:55 -05001105 query.update(self.params)
Joe Gregorio7c22ab22011-02-16 15:32:39 -05001106 parts = list(urlparse.urlparse(self.auth_uri))
Joe Gregorio695fdc12011-01-16 16:46:55 -05001107 query.update(dict(parse_qsl(parts[4]))) # 4 is the index of the query part
1108 parts[4] = urllib.urlencode(query)
1109 return urlparse.urlunparse(parts)
1110
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001111 @util.positional(2)
Joe Gregorioccc79542011-02-19 00:05:26 -05001112 def step2_exchange(self, code, http=None):
Joe Gregorio695fdc12011-01-16 16:46:55 -05001113 """Exhanges a code for OAuth2Credentials.
1114
1115 Args:
1116 code: string or dict, either the code as a string, or a dictionary
1117 of the query parameters to the redirect_uri, which contains
1118 the code.
Joe Gregorioccc79542011-02-19 00:05:26 -05001119 http: httplib2.Http, optional http instance to use to do the fetch
Joe Gregorio4b4002f2012-06-14 15:41:01 -04001120
1121 Returns:
1122 An OAuth2Credentials object that can be used to authorize requests.
1123
1124 Raises:
1125 FlowExchangeError if a problem occured exchanging the code for a
1126 refresh_token.
Joe Gregorio695fdc12011-01-16 16:46:55 -05001127 """
1128
1129 if not (isinstance(code, str) or isinstance(code, unicode)):
Joe Gregorio4b4002f2012-06-14 15:41:01 -04001130 if 'code' not in code:
1131 if 'error' in code:
1132 error_msg = code['error']
1133 else:
1134 error_msg = 'No code was supplied in the query parameters.'
1135 raise FlowExchangeError(error_msg)
1136 else:
1137 code = code['code']
Joe Gregorio695fdc12011-01-16 16:46:55 -05001138
1139 body = urllib.urlencode({
Joe Gregorio9da2ad82011-09-11 14:04:44 -04001140 'grant_type': 'authorization_code',
1141 'client_id': self.client_id,
1142 'client_secret': self.client_secret,
1143 'code': code,
1144 'redirect_uri': self.redirect_uri,
1145 'scope': self.scope,
1146 })
Joe Gregorio695fdc12011-01-16 16:46:55 -05001147 headers = {
Joe Gregorio9da2ad82011-09-11 14:04:44 -04001148 'content-type': 'application/x-www-form-urlencoded',
Joe Gregorio695fdc12011-01-16 16:46:55 -05001149 }
JacobMoshenkocb6d8912011-07-08 13:35:15 -04001150
1151 if self.user_agent is not None:
1152 headers['user-agent'] = self.user_agent
1153
Joe Gregorioccc79542011-02-19 00:05:26 -05001154 if http is None:
1155 http = httplib2.Http()
Joe Gregorio8b4c1732011-12-06 11:28:29 -05001156
JacobMoshenko8e905102011-06-20 09:53:10 -04001157 resp, content = http.request(self.token_uri, method='POST', body=body,
1158 headers=headers)
Joe Gregorioddb969a2012-07-11 11:04:12 -04001159 d = _parse_exchange_token_response(content)
1160 if resp.status == 200 and 'access_token' in d:
Joe Gregorio695fdc12011-01-16 16:46:55 -05001161 access_token = d['access_token']
1162 refresh_token = d.get('refresh_token', None)
1163 token_expiry = None
1164 if 'expires_in' in d:
Joe Gregorio562b7312011-09-15 09:06:38 -04001165 token_expiry = datetime.datetime.utcnow() + datetime.timedelta(
JacobMoshenko8e905102011-06-20 09:53:10 -04001166 seconds=int(d['expires_in']))
Joe Gregorio695fdc12011-01-16 16:46:55 -05001167
Joe Gregorio8b4c1732011-12-06 11:28:29 -05001168 if 'id_token' in d:
1169 d['id_token'] = _extract_id_token(d['id_token'])
1170
Joe Gregorio6ceea2d2012-08-24 11:57:58 -04001171 logger.info('Successfully retrieved access token')
JacobMoshenko8e905102011-06-20 09:53:10 -04001172 return OAuth2Credentials(access_token, self.client_id,
1173 self.client_secret, refresh_token, token_expiry,
Joe Gregorio8b4c1732011-12-06 11:28:29 -05001174 self.token_uri, self.user_agent,
1175 id_token=d.get('id_token', None))
Joe Gregorio695fdc12011-01-16 16:46:55 -05001176 else:
Joe Gregorioe78621a2012-03-09 15:47:23 -05001177 logger.info('Failed to retrieve access token: %s' % content)
Joe Gregorioddb969a2012-07-11 11:04:12 -04001178 if 'error' in d:
1179 # you never know what those providers got to say
1180 error_msg = unicode(d['error'])
1181 else:
1182 error_msg = 'Invalid response: %s.' % str(resp.status)
Joe Gregorioccc79542011-02-19 00:05:26 -05001183 raise FlowExchangeError(error_msg)
Joe Gregoriof08a4982011-10-07 13:11:16 -04001184
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001185
1186@util.positional(2)
1187def flow_from_clientsecrets(filename, scope, redirect_uri=None, message=None, cache=None):
Joe Gregoriof08a4982011-10-07 13:11:16 -04001188 """Create a Flow from a clientsecrets file.
1189
1190 Will create the right kind of Flow based on the contents of the clientsecrets
1191 file or will raise InvalidClientSecretsError for unknown types of Flows.
1192
1193 Args:
1194 filename: string, File name of client secrets.
Joe Gregorio5cf5d122012-11-16 16:36:12 -05001195 scope: string or iterable of strings, scope(s) to request.
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001196 redirect_uri: string, Either the string 'urn:ietf:wg:oauth:2.0:oob' for
1197 a non-web-based application, or a URI that handles the callback from
1198 the authorization server.
Joe Gregoriof08a4982011-10-07 13:11:16 -04001199 message: string, A friendly string to display to the user if the
1200 clientsecrets file is missing or invalid. If message is provided then
1201 sys.exit will be called in the case of an error. If message in not
1202 provided then clientsecrets.InvalidClientSecretsError will be raised.
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001203 cache: An optional cache service client that implements get() and set()
Joe Gregorioc29aaa92012-07-16 16:16:31 -04001204 methods. See clientsecrets.loadfile() for details.
Joe Gregoriof08a4982011-10-07 13:11:16 -04001205
1206 Returns:
1207 A Flow object.
1208
1209 Raises:
1210 UnknownClientSecretsFlowError if the file describes an unknown kind of Flow.
1211 clientsecrets.InvalidClientSecretsError if the clientsecrets file is
1212 invalid.
1213 """
Joe Gregorio0984ef22011-10-14 13:17:43 -04001214 try:
Joe Gregorioc29aaa92012-07-16 16:16:31 -04001215 client_type, client_info = clientsecrets.loadfile(filename, cache=cache)
Joe Gregorio0984ef22011-10-14 13:17:43 -04001216 if client_type in [clientsecrets.TYPE_WEB, clientsecrets.TYPE_INSTALLED]:
1217 return OAuth2WebServerFlow(
1218 client_info['client_id'],
1219 client_info['client_secret'],
1220 scope,
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001221 redirect_uri=redirect_uri,
1222 user_agent=None,
1223 auth_uri=client_info['auth_uri'],
1224 token_uri=client_info['token_uri'])
1225
Joe Gregorio0984ef22011-10-14 13:17:43 -04001226 except clientsecrets.InvalidClientSecretsError:
1227 if message:
1228 sys.exit(message)
1229 else:
1230 raise
Joe Gregoriof08a4982011-10-07 13:11:16 -04001231 else:
1232 raise UnknownClientSecretsFlowError(
1233 'This OAuth 2.0 flow is unsupported: "%s"' * client_type)