blob: 894bfb4a308adf78a7f572d3a8d952ed588c107b [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
22import copy
23import datetime
24import httplib2
25import logging
26import urllib
27import urlparse
28
Joe Gregorio9da2ad82011-09-11 14:04:44 -040029try: # pragma: no cover
Joe Gregorio695fdc12011-01-16 16:46:55 -050030 import simplejson
Joe Gregorio9da2ad82011-09-11 14:04:44 -040031except ImportError: # pragma: no cover
Joe Gregorio695fdc12011-01-16 16:46:55 -050032 try:
33 # Try to import from django, should work on App Engine
34 from django.utils import simplejson
35 except ImportError:
36 # Should work for Python2.6 and higher.
37 import json as simplejson
38
39try:
Joe Gregorio9da2ad82011-09-11 14:04:44 -040040 from urlparse import parse_qsl
Joe Gregorio695fdc12011-01-16 16:46:55 -050041except ImportError:
Joe Gregorio9da2ad82011-09-11 14:04:44 -040042 from cgi import parse_qsl
43
44logger = logging.getLogger(__name__)
Joe Gregorio695fdc12011-01-16 16:46:55 -050045
46
47class Error(Exception):
48 """Base error for this module."""
49 pass
50
51
Joe Gregorioccc79542011-02-19 00:05:26 -050052class FlowExchangeError(Error):
Joe Gregorioca876e42011-02-22 19:39:42 -050053 """Error trying to exchange an authorization grant for an access token."""
Joe Gregorioccc79542011-02-19 00:05:26 -050054 pass
55
56
57class AccessTokenRefreshError(Error):
Joe Gregorioca876e42011-02-22 19:39:42 -050058 """Error trying to refresh an expired access token."""
Joe Gregorio695fdc12011-01-16 16:46:55 -050059 pass
60
61
Joe Gregorio3b79fa82011-02-17 11:47:17 -050062class AccessTokenCredentialsError(Error):
63 """Having only the access_token means no refresh is possible."""
Joe Gregorio695fdc12011-01-16 16:46:55 -050064 pass
65
66
67def _abstract():
68 raise NotImplementedError('You need to override this function')
69
70
71class Credentials(object):
72 """Base class for all Credentials objects.
73
74 Subclasses must define an authorize() method
75 that applies the credentials to an HTTP transport.
76 """
77
78 def authorize(self, http):
79 """Take an httplib2.Http instance (or equivalent) and
80 authorizes it for the set of credentials, usually by
81 replacing http.request() with a method that adds in
82 the appropriate headers and then delegates to the original
83 Http.request() method.
84 """
85 _abstract()
86
JacobMoshenko8e905102011-06-20 09:53:10 -040087
Joe Gregorio695fdc12011-01-16 16:46:55 -050088class Flow(object):
89 """Base class for all Flow objects."""
90 pass
91
92
Joe Gregoriodeeb0202011-02-15 14:49:57 -050093class Storage(object):
94 """Base class for all Storage objects.
95
Joe Gregorio9da2ad82011-09-11 14:04:44 -040096 Store and retrieve a single credential. This class supports locking
97 such that multiple processes and threads can operate on a single
98 store.
Joe Gregoriodeeb0202011-02-15 14:49:57 -050099 """
100
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400101 def acquire_lock(self):
102 """Acquires any lock necessary to access this Storage.
103
104 This lock is not reentrant."""
105 pass
106
107 def release_lock(self):
108 """Release the Storage lock.
109
110 Trying to release a lock that isn't held will result in a
111 RuntimeError.
112 """
113 pass
114
115 def locked_get(self):
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500116 """Retrieve credential.
117
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400118 The Storage lock must be held when this is called.
119
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500120 Returns:
Joe Gregorio06d852b2011-03-25 15:03:10 -0400121 oauth2client.client.Credentials
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500122 """
123 _abstract()
124
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400125 def locked_put(self, credentials):
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500126 """Write a credential.
127
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400128 The Storage lock must be held when this is called.
129
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500130 Args:
131 credentials: Credentials, the credentials to store.
132 """
133 _abstract()
134
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400135 def get(self):
136 """Retrieve credential.
137
138 The Storage lock must *not* be held when this is called.
139
140 Returns:
141 oauth2client.client.Credentials
142 """
143 self.acquire_lock()
144 try:
145 return self.locked_get()
146 finally:
147 self.release_lock()
148
149 def put(self, credentials):
150 """Write a credential.
151
152 The Storage lock must be held when this is called.
153
154 Args:
155 credentials: Credentials, the credentials to store.
156 """
157 self.acquire_lock()
158 try:
159 self.locked_put(credentials)
160 finally:
161 self.release_lock()
162
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500163
Joe Gregorio695fdc12011-01-16 16:46:55 -0500164class OAuth2Credentials(Credentials):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400165 """Credentials object for OAuth 2.0.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500166
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500167 Credentials can be applied to an httplib2.Http object using the authorize()
168 method, which then signs each request from that object with the OAuth 2.0
169 access token.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500170
171 OAuth2Credentials objects may be safely pickled and unpickled.
172 """
173
174 def __init__(self, access_token, client_id, client_secret, refresh_token,
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400175 token_expiry, token_uri, user_agent):
176 """Create an instance of OAuth2Credentials.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500177
178 This constructor is not usually called by the user, instead
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500179 OAuth2Credentials objects are instantiated by the OAuth2WebServerFlow.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500180
181 Args:
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400182 access_token: string, access token.
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500183 client_id: string, client identifier.
184 client_secret: string, client secret.
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500185 refresh_token: string, refresh token.
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400186 token_expiry: datetime, when the access_token expires.
187 token_uri: string, URI of token endpoint.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500188 user_agent: string, The HTTP User-Agent to provide for this application.
189
Joe Gregorio695fdc12011-01-16 16:46:55 -0500190 Notes:
191 store: callable, a callable that when passed a Credential
192 will store the credential back to where it came from.
193 This is needed to store the latest access_token if it
194 has expired and been refreshed.
195 """
196 self.access_token = access_token
197 self.client_id = client_id
198 self.client_secret = client_secret
199 self.refresh_token = refresh_token
200 self.store = None
201 self.token_expiry = token_expiry
202 self.token_uri = token_uri
203 self.user_agent = user_agent
204
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500205 # True if the credentials have been revoked or expired and can't be
206 # refreshed.
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400207 self.invalid = False
Joe Gregorio9ce4b622011-02-17 15:32:11 -0500208
209 @property
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400210 def access_token_expired(self):
211 """True if the credential is expired or invalid.
212
213 If the token_expiry isn't set, we assume the token doesn't expire.
214 """
215 if self.invalid:
216 return True
217
218 if not self.token_expiry:
219 return False
220
221 now = datetime.datetime.now()
222 if now >= self.token_expiry:
223 logger.info('access_token is expired. Now: %s, token_expiry: %s',
224 now, self.token_expiry)
225 return True
226 return False
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500227
Joe Gregorio695fdc12011-01-16 16:46:55 -0500228 def set_store(self, store):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400229 """Set the Storage for the credential.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500230
231 Args:
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400232 store: Storage, an implementation of Stroage object.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500233 This is needed to store the latest access_token if it
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400234 has expired and been refreshed. This implementation uses
235 locking to check for updates before updating the
236 access_token.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500237 """
238 self.store = store
239
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400240 def _updateFromCredential(self, other):
241 """Update this Credential from another instance."""
242 self.__dict__.update(other.__getstate__())
243
Joe Gregorio695fdc12011-01-16 16:46:55 -0500244 def __getstate__(self):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400245 """Trim the state down to something that can be pickled."""
Joe Gregorio695fdc12011-01-16 16:46:55 -0500246 d = copy.copy(self.__dict__)
247 del d['store']
248 return d
249
250 def __setstate__(self, state):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400251 """Reconstitute the state of the object from being pickled."""
Joe Gregorio695fdc12011-01-16 16:46:55 -0500252 self.__dict__.update(state)
253 self.store = None
254
JacobMoshenko8e905102011-06-20 09:53:10 -0400255 def _generate_refresh_request_body(self):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400256 """Generate the body that will be used in the refresh request."""
JacobMoshenko8e905102011-06-20 09:53:10 -0400257 body = urllib.urlencode({
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400258 'grant_type': 'refresh_token',
259 'client_id': self.client_id,
260 'client_secret': self.client_secret,
261 'refresh_token': self.refresh_token,
262 })
JacobMoshenko8e905102011-06-20 09:53:10 -0400263 return body
264
265 def _generate_refresh_request_headers(self):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400266 """Generate the headers that will be used in the refresh request."""
JacobMoshenko8e905102011-06-20 09:53:10 -0400267 headers = {
JacobMoshenko8e905102011-06-20 09:53:10 -0400268 'content-type': 'application/x-www-form-urlencoded',
269 }
JacobMoshenkocb6d8912011-07-08 13:35:15 -0400270
271 if self.user_agent is not None:
272 headers['user-agent'] = self.user_agent
273
JacobMoshenko8e905102011-06-20 09:53:10 -0400274 return headers
275
Joe Gregorio695fdc12011-01-16 16:46:55 -0500276 def _refresh(self, http_request):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400277 """Refreshes the access_token.
278
279 This method first checks by reading the Storage object if available.
280 If a refresh is still needed, it holds the Storage lock until the
281 refresh is completed.
282 """
283 if not self.store:
284 self._do_refresh_request(http_request)
285 else:
286 self.store.acquire_lock()
287 try:
288 new_cred = self.store.locked_get()
289 if (new_cred and not new_cred.invalid and
290 new_cred.access_token != self.access_token):
291 logger.info('Updated access_token read from Storage')
292 self._updateFromCredential(new_cred)
293 else:
294 self._do_refresh_request(http_request)
295 finally:
296 self.store.release_lock()
297
298 def _do_refresh_request(self, http_request):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500299 """Refresh the access_token using the refresh_token.
300
301 Args:
302 http: An instance of httplib2.Http.request
303 or something that acts like it.
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400304
305 Raises:
306 AccessTokenRefreshError: When the refresh fails.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500307 """
JacobMoshenko8e905102011-06-20 09:53:10 -0400308 body = self._generate_refresh_request_body()
309 headers = self._generate_refresh_request_headers()
310
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400311 logger.info('Refresing access_token')
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500312 resp, content = http_request(
313 self.token_uri, method='POST', body=body, headers=headers)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500314 if resp.status == 200:
315 # TODO(jcgregorio) Raise an error if loads fails?
316 d = simplejson.loads(content)
317 self.access_token = d['access_token']
318 self.refresh_token = d.get('refresh_token', self.refresh_token)
319 if 'expires_in' in d:
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500320 self.token_expiry = datetime.timedelta(
JacobMoshenko8e905102011-06-20 09:53:10 -0400321 seconds=int(d['expires_in'])) + datetime.datetime.now()
Joe Gregorio695fdc12011-01-16 16:46:55 -0500322 else:
323 self.token_expiry = None
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400324 if self.store:
325 self.store.locked_put(self)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500326 else:
JacobMoshenko8e905102011-06-20 09:53:10 -0400327 # An {'error':...} response body means the token is expired or revoked,
328 # so we flag the credentials as such.
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400329 logger.error('Failed to retrieve access token: %s' % content)
Joe Gregorioccc79542011-02-19 00:05:26 -0500330 error_msg = 'Invalid response %s.' % resp['status']
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500331 try:
332 d = simplejson.loads(content)
333 if 'error' in d:
Joe Gregorioccc79542011-02-19 00:05:26 -0500334 error_msg = d['error']
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400335 self.invalid = True
336 if self.store:
337 self.store.locked_put(self)
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500338 except:
339 pass
Joe Gregorioccc79542011-02-19 00:05:26 -0500340 raise AccessTokenRefreshError(error_msg)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500341
342 def authorize(self, http):
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500343 """Authorize an httplib2.Http instance with these credentials.
344
Joe Gregorio695fdc12011-01-16 16:46:55 -0500345 Args:
346 http: An instance of httplib2.Http
347 or something that acts like it.
348
349 Returns:
350 A modified instance of http that was passed in.
351
352 Example:
353
354 h = httplib2.Http()
355 h = credentials.authorize(h)
356
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400357 You can't create a new OAuth subclass of httplib2.Authenication
358 because it never gets passed the absolute URI, which is needed for
359 signing. So instead we have to overload 'request' with a closure
360 that adds in the Authorization header and then calls the original
361 version of 'request()'.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500362 """
363 request_orig = http.request
364
365 # The closure that will replace 'httplib2.Http.request'.
366 def new_request(uri, method='GET', body=None, headers=None,
367 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
368 connection_type=None):
JacobMoshenko8e905102011-06-20 09:53:10 -0400369 if not self.access_token:
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400370 logger.info('Attempting refresh to obtain initial access_token')
JacobMoshenko8e905102011-06-20 09:53:10 -0400371 self._refresh(request_orig)
372
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400373 # Modify the request headers to add the appropriate
374 # Authorization header.
375 if headers is None:
Joe Gregorio695fdc12011-01-16 16:46:55 -0500376 headers = {}
Joe Gregorio49e94d82011-01-28 16:36:13 -0500377 headers['authorization'] = 'OAuth ' + self.access_token
JacobMoshenkocb6d8912011-07-08 13:35:15 -0400378
379 if self.user_agent is not None:
380 if 'user-agent' in headers:
381 headers['user-agent'] = self.user_agent + ' ' + headers['user-agent']
382 else:
383 headers['user-agent'] = self.user_agent
JacobMoshenko8e905102011-06-20 09:53:10 -0400384
Joe Gregorio695fdc12011-01-16 16:46:55 -0500385 resp, content = request_orig(uri, method, body, headers,
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500386 redirections, connection_type)
JacobMoshenko8e905102011-06-20 09:53:10 -0400387
Joe Gregoriofd19cd32011-01-20 11:37:29 -0500388 if resp.status == 401:
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400389 logger.info('Refreshing due to a 401')
Joe Gregorio695fdc12011-01-16 16:46:55 -0500390 self._refresh(request_orig)
Joe Gregorioccc79542011-02-19 00:05:26 -0500391 headers['authorization'] = 'OAuth ' + self.access_token
Joe Gregorio695fdc12011-01-16 16:46:55 -0500392 return request_orig(uri, method, body, headers,
393 redirections, connection_type)
394 else:
395 return (resp, content)
396
397 http.request = new_request
398 return http
399
400
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500401class AccessTokenCredentials(OAuth2Credentials):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400402 """Credentials object for OAuth 2.0.
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500403
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400404 Credentials can be applied to an httplib2.Http object using the
405 authorize() method, which then signs each request from that object
406 with the OAuth 2.0 access token. This set of credentials is for the
407 use case where you have acquired an OAuth 2.0 access_token from
408 another place such as a JavaScript client or another web
409 application, and wish to use it from Python. Because only the
410 access_token is present it can not be refreshed and will in time
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500411 expire.
412
Joe Gregorio9ce4b622011-02-17 15:32:11 -0500413 AccessTokenCredentials objects may be safely pickled and unpickled.
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500414
415 Usage:
416 credentials = AccessTokenCredentials('<an access token>',
417 'my-user-agent/1.0')
418 http = httplib2.Http()
419 http = credentials.authorize(http)
420
421 Exceptions:
422 AccessTokenCredentialsExpired: raised when the access_token expires or is
423 revoked.
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500424 """
425
426 def __init__(self, access_token, user_agent):
427 """Create an instance of OAuth2Credentials
428
429 This is one of the few types if Credentials that you should contrust,
430 Credentials objects are usually instantiated by a Flow.
431
432 Args:
ade@google.com93a7f7c2011-02-23 16:00:37 +0000433 access_token: string, access token.
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500434 user_agent: string, The HTTP User-Agent to provide for this application.
435
436 Notes:
437 store: callable, a callable that when passed a Credential
438 will store the credential back to where it came from.
439 """
440 super(AccessTokenCredentials, self).__init__(
441 access_token,
442 None,
443 None,
444 None,
445 None,
446 None,
447 user_agent)
448
449 def _refresh(self, http_request):
450 raise AccessTokenCredentialsError(
451 "The access_token is expired or invalid and can't be refreshed.")
452
JacobMoshenko8e905102011-06-20 09:53:10 -0400453
454class AssertionCredentials(OAuth2Credentials):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400455 """Abstract Credentials object used for OAuth 2.0 assertion grants.
JacobMoshenko8e905102011-06-20 09:53:10 -0400456
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400457 This credential does not require a flow to instantiate because it
458 represents a two legged flow, and therefore has all of the required
459 information to generate and refresh its own access tokens. It must
460 be subclassed to generate the appropriate assertion string.
JacobMoshenko8e905102011-06-20 09:53:10 -0400461
462 AssertionCredentials objects may be safely pickled and unpickled.
463 """
464
465 def __init__(self, assertion_type, user_agent,
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400466 token_uri='https://accounts.google.com/o/oauth2/token',
467 **unused_kwargs):
468 """Constructor for AssertionFlowCredentials.
JacobMoshenko8e905102011-06-20 09:53:10 -0400469
470 Args:
471 assertion_type: string, assertion type that will be declared to the auth
472 server
473 user_agent: string, The HTTP User-Agent to provide for this application.
474 token_uri: string, URI for token endpoint. For convenience
475 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
476 """
477 super(AssertionCredentials, self).__init__(
478 None,
479 None,
480 None,
481 None,
482 None,
483 token_uri,
484 user_agent)
485 self.assertion_type = assertion_type
486
487 def _generate_refresh_request_body(self):
488 assertion = self._generate_assertion()
489
490 body = urllib.urlencode({
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400491 'assertion_type': self.assertion_type,
492 'assertion': assertion,
493 'grant_type': 'assertion',
494 })
JacobMoshenko8e905102011-06-20 09:53:10 -0400495
496 return body
497
498 def _generate_assertion(self):
499 """Generate the assertion string that will be used in the access token
500 request.
501 """
502 _abstract()
503
504
Joe Gregorio695fdc12011-01-16 16:46:55 -0500505class OAuth2WebServerFlow(Flow):
506 """Does the Web Server Flow for OAuth 2.0.
507
508 OAuth2Credentials objects may be safely pickled and unpickled.
509 """
510
511 def __init__(self, client_id, client_secret, scope, user_agent,
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400512 auth_uri='https://accounts.google.com/o/oauth2/auth',
513 token_uri='https://accounts.google.com/o/oauth2/token',
514 **kwargs):
515 """Constructor for OAuth2WebServerFlow.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500516
517 Args:
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500518 client_id: string, client identifier.
519 client_secret: string client secret.
520 scope: string, scope of the credentials being requested.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500521 user_agent: string, HTTP User-Agent to provide for this application.
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500522 auth_uri: string, URI for authorization endpoint. For convenience
523 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
524 token_uri: string, URI for token endpoint. For convenience
525 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500526 **kwargs: dict, The keyword arguments are all optional and required
527 parameters for the OAuth calls.
528 """
529 self.client_id = client_id
530 self.client_secret = client_secret
531 self.scope = scope
532 self.user_agent = user_agent
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500533 self.auth_uri = auth_uri
Joe Gregorio695fdc12011-01-16 16:46:55 -0500534 self.token_uri = token_uri
535 self.params = kwargs
536 self.redirect_uri = None
537
538 def step1_get_authorize_url(self, redirect_uri='oob'):
539 """Returns a URI to redirect to the provider.
540
541 Args:
542 redirect_uri: string, Either the string 'oob' for a non-web-based
543 application, or a URI that handles the callback from
544 the authorization server.
545
546 If redirect_uri is 'oob' then pass in the
547 generated verification code to step2_exchange,
548 otherwise pass in the query parameters received
549 at the callback uri to step2_exchange.
550 """
551
552 self.redirect_uri = redirect_uri
553 query = {
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400554 'response_type': 'code',
555 'client_id': self.client_id,
556 'redirect_uri': redirect_uri,
557 'scope': self.scope,
558 }
Joe Gregorio695fdc12011-01-16 16:46:55 -0500559 query.update(self.params)
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500560 parts = list(urlparse.urlparse(self.auth_uri))
Joe Gregorio695fdc12011-01-16 16:46:55 -0500561 query.update(dict(parse_qsl(parts[4]))) # 4 is the index of the query part
562 parts[4] = urllib.urlencode(query)
563 return urlparse.urlunparse(parts)
564
Joe Gregorioccc79542011-02-19 00:05:26 -0500565 def step2_exchange(self, code, http=None):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500566 """Exhanges a code for OAuth2Credentials.
567
568 Args:
569 code: string or dict, either the code as a string, or a dictionary
570 of the query parameters to the redirect_uri, which contains
571 the code.
Joe Gregorioccc79542011-02-19 00:05:26 -0500572 http: httplib2.Http, optional http instance to use to do the fetch
Joe Gregorio695fdc12011-01-16 16:46:55 -0500573 """
574
575 if not (isinstance(code, str) or isinstance(code, unicode)):
576 code = code['code']
577
578 body = urllib.urlencode({
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400579 'grant_type': 'authorization_code',
580 'client_id': self.client_id,
581 'client_secret': self.client_secret,
582 'code': code,
583 'redirect_uri': self.redirect_uri,
584 'scope': self.scope,
585 })
Joe Gregorio695fdc12011-01-16 16:46:55 -0500586 headers = {
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400587 'user-agent': self.user_agent,
588 'content-type': 'application/x-www-form-urlencoded',
Joe Gregorio695fdc12011-01-16 16:46:55 -0500589 }
JacobMoshenkocb6d8912011-07-08 13:35:15 -0400590
591 if self.user_agent is not None:
592 headers['user-agent'] = self.user_agent
593
Joe Gregorioccc79542011-02-19 00:05:26 -0500594 if http is None:
595 http = httplib2.Http()
JacobMoshenko8e905102011-06-20 09:53:10 -0400596 resp, content = http.request(self.token_uri, method='POST', body=body,
597 headers=headers)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500598 if resp.status == 200:
599 # TODO(jcgregorio) Raise an error if simplejson.loads fails?
600 d = simplejson.loads(content)
601 access_token = d['access_token']
602 refresh_token = d.get('refresh_token', None)
603 token_expiry = None
604 if 'expires_in' in d:
JacobMoshenko8e905102011-06-20 09:53:10 -0400605 token_expiry = datetime.datetime.now() + datetime.timedelta(
606 seconds=int(d['expires_in']))
Joe Gregorio695fdc12011-01-16 16:46:55 -0500607
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400608 logger.info('Successfully retrieved access token: %s' % content)
JacobMoshenko8e905102011-06-20 09:53:10 -0400609 return OAuth2Credentials(access_token, self.client_id,
610 self.client_secret, refresh_token, token_expiry,
611 self.token_uri, self.user_agent)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500612 else:
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400613 logger.error('Failed to retrieve access token: %s' % content)
Joe Gregorioccc79542011-02-19 00:05:26 -0500614 error_msg = 'Invalid response %s.' % resp['status']
615 try:
616 d = simplejson.loads(content)
617 if 'error' in d:
618 error_msg = d['error']
619 except:
620 pass
621
622 raise FlowExchangeError(error_msg)