blob: cce4ae6fa96286e67788766dfef9c54fe6688fd3 [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 Gregorio695fdc12011-01-16 16:46:55 -050092def _abstract():
93 raise NotImplementedError('You need to override this function')
94
95
Joe Gregorio9f2f38f2012-02-06 12:53:00 -050096class MemoryCache(object):
97 """httplib2 Cache implementation which only caches locally."""
98
99 def __init__(self):
100 self.cache = {}
101
102 def get(self, key):
103 return self.cache.get(key)
104
105 def set(self, key, value):
106 self.cache[key] = value
107
108 def delete(self, key):
109 self.cache.pop(key, None)
110
111
Joe Gregorio695fdc12011-01-16 16:46:55 -0500112class Credentials(object):
113 """Base class for all Credentials objects.
114
Joe Gregorio562b7312011-09-15 09:06:38 -0400115 Subclasses must define an authorize() method that applies the credentials to
116 an HTTP transport.
117
118 Subclasses must also specify a classmethod named 'from_json' that takes a JSON
Joe Gregoriofa8cd9f2012-02-23 14:00:40 -0500119 string as input and returns an instaniated Credentials object.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500120 """
121
Joe Gregorio562b7312011-09-15 09:06:38 -0400122 NON_SERIALIZED_MEMBERS = ['store']
123
Joe Gregorio695fdc12011-01-16 16:46:55 -0500124 def authorize(self, http):
125 """Take an httplib2.Http instance (or equivalent) and
126 authorizes it for the set of credentials, usually by
127 replacing http.request() with a method that adds in
128 the appropriate headers and then delegates to the original
129 Http.request() method.
130 """
131 _abstract()
132
Joe Gregorio654f4a22012-02-09 14:15:44 -0500133 def refresh(self, http):
134 """Forces a refresh of the access_token.
135
136 Args:
137 http: httplib2.Http, an http object to be used to make the refresh
138 request.
139 """
140 _abstract()
141
142 def apply(self, headers):
143 """Add the authorization to the headers.
144
145 Args:
146 headers: dict, the headers to add the Authorization header to.
147 """
148 _abstract()
149
Joe Gregorio562b7312011-09-15 09:06:38 -0400150 def _to_json(self, strip):
151 """Utility function for creating a JSON representation of an instance of Credentials.
152
153 Args:
154 strip: array, An array of names of members to not include in the JSON.
155
156 Returns:
157 string, a JSON representation of this instance, suitable to pass to
158 from_json().
159 """
160 t = type(self)
161 d = copy.copy(self.__dict__)
162 for member in strip:
Joe Gregorio08cdcb82012-03-14 00:09:33 -0400163 if member in d:
164 del d[member]
Joe Gregorio562b7312011-09-15 09:06:38 -0400165 if 'token_expiry' in d and isinstance(d['token_expiry'], datetime.datetime):
166 d['token_expiry'] = d['token_expiry'].strftime(EXPIRY_FORMAT)
167 # Add in information we will need later to reconsistitue this instance.
168 d['_class'] = t.__name__
169 d['_module'] = t.__module__
170 return simplejson.dumps(d)
171
172 def to_json(self):
173 """Creating a JSON representation of an instance of Credentials.
174
175 Returns:
176 string, a JSON representation of this instance, suitable to pass to
177 from_json().
178 """
179 return self._to_json(Credentials.NON_SERIALIZED_MEMBERS)
180
181 @classmethod
182 def new_from_json(cls, s):
183 """Utility class method to instantiate a Credentials subclass from a JSON
184 representation produced by to_json().
185
186 Args:
187 s: string, JSON from to_json().
188
189 Returns:
190 An instance of the subclass of Credentials that was serialized with
191 to_json().
192 """
193 data = simplejson.loads(s)
194 # Find and call the right classmethod from_json() to restore the object.
195 module = data['_module']
Joe Gregoriofa8cd9f2012-02-23 14:00:40 -0500196 try:
197 m = __import__(module)
198 except ImportError:
199 # In case there's an object from the old package structure, update it
200 module = module.replace('.apiclient', '')
201 m = __import__(module)
202
Joe Gregorioe9b40f12011-10-13 10:03:28 -0400203 m = __import__(module, fromlist=module.split('.')[:-1])
Joe Gregorio562b7312011-09-15 09:06:38 -0400204 kls = getattr(m, data['_class'])
205 from_json = getattr(kls, 'from_json')
206 return from_json(s)
207
Joe Gregorio08cdcb82012-03-14 00:09:33 -0400208 @classmethod
209 def from_json(cls, s):
Joe Gregorio401b8422012-05-03 16:35:35 -0400210 """Instantiate a Credentials object from a JSON description of it.
211
212 The JSON should have been produced by calling .to_json() on the object.
Joe Gregorio08cdcb82012-03-14 00:09:33 -0400213
214 Args:
215 data: dict, A deserialized JSON object.
216
217 Returns:
218 An instance of a Credentials subclass.
219 """
220 return Credentials()
221
JacobMoshenko8e905102011-06-20 09:53:10 -0400222
Joe Gregorio695fdc12011-01-16 16:46:55 -0500223class Flow(object):
224 """Base class for all Flow objects."""
225 pass
226
227
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500228class Storage(object):
229 """Base class for all Storage objects.
230
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400231 Store and retrieve a single credential. This class supports locking
232 such that multiple processes and threads can operate on a single
233 store.
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500234 """
235
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400236 def acquire_lock(self):
237 """Acquires any lock necessary to access this Storage.
238
Joe Gregorio401b8422012-05-03 16:35:35 -0400239 This lock is not reentrant.
240 """
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400241 pass
242
243 def release_lock(self):
244 """Release the Storage lock.
245
246 Trying to release a lock that isn't held will result in a
247 RuntimeError.
248 """
249 pass
250
251 def locked_get(self):
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500252 """Retrieve credential.
253
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400254 The Storage lock must be held when this is called.
255
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500256 Returns:
Joe Gregorio06d852b2011-03-25 15:03:10 -0400257 oauth2client.client.Credentials
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500258 """
259 _abstract()
260
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400261 def locked_put(self, credentials):
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500262 """Write a credential.
263
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400264 The Storage lock must be held when this is called.
265
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500266 Args:
267 credentials: Credentials, the credentials to store.
268 """
269 _abstract()
270
Joe Gregorioec75dc12012-02-06 13:40:42 -0500271 def locked_delete(self):
272 """Delete a credential.
273
274 The Storage lock must be held when this is called.
275 """
276 _abstract()
277
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400278 def get(self):
279 """Retrieve credential.
280
281 The Storage lock must *not* be held when this is called.
282
283 Returns:
284 oauth2client.client.Credentials
285 """
286 self.acquire_lock()
287 try:
288 return self.locked_get()
289 finally:
290 self.release_lock()
291
292 def put(self, credentials):
293 """Write a credential.
294
295 The Storage lock must be held when this is called.
296
297 Args:
298 credentials: Credentials, the credentials to store.
299 """
300 self.acquire_lock()
301 try:
302 self.locked_put(credentials)
303 finally:
304 self.release_lock()
305
Joe Gregorioec75dc12012-02-06 13:40:42 -0500306 def delete(self):
307 """Delete credential.
308
309 Frees any resources associated with storing the credential.
310 The Storage lock must *not* be held when this is called.
311
312 Returns:
313 None
314 """
315 self.acquire_lock()
316 try:
317 return self.locked_delete()
318 finally:
319 self.release_lock()
320
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500321
Joe Gregorio695fdc12011-01-16 16:46:55 -0500322class OAuth2Credentials(Credentials):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400323 """Credentials object for OAuth 2.0.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500324
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500325 Credentials can be applied to an httplib2.Http object using the authorize()
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500326 method, which then adds the OAuth 2.0 access token to each request.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500327
328 OAuth2Credentials objects may be safely pickled and unpickled.
329 """
330
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400331 @util.positional(8)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500332 def __init__(self, access_token, client_id, client_secret, refresh_token,
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500333 token_expiry, token_uri, user_agent, id_token=None):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400334 """Create an instance of OAuth2Credentials.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500335
336 This constructor is not usually called by the user, instead
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500337 OAuth2Credentials objects are instantiated by the OAuth2WebServerFlow.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500338
339 Args:
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400340 access_token: string, access token.
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500341 client_id: string, client identifier.
342 client_secret: string, client secret.
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500343 refresh_token: string, refresh token.
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400344 token_expiry: datetime, when the access_token expires.
345 token_uri: string, URI of token endpoint.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500346 user_agent: string, The HTTP User-Agent to provide for this application.
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500347 id_token: object, The identity of the resource owner.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500348
Joe Gregorio695fdc12011-01-16 16:46:55 -0500349 Notes:
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500350 store: callable, A callable that when passed a Credential
Joe Gregorio695fdc12011-01-16 16:46:55 -0500351 will store the credential back to where it came from.
352 This is needed to store the latest access_token if it
353 has expired and been refreshed.
354 """
355 self.access_token = access_token
356 self.client_id = client_id
357 self.client_secret = client_secret
358 self.refresh_token = refresh_token
359 self.store = None
360 self.token_expiry = token_expiry
361 self.token_uri = token_uri
362 self.user_agent = user_agent
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500363 self.id_token = id_token
Joe Gregorio695fdc12011-01-16 16:46:55 -0500364
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500365 # True if the credentials have been revoked or expired and can't be
366 # refreshed.
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400367 self.invalid = False
Joe Gregorio9ce4b622011-02-17 15:32:11 -0500368
Joe Gregorio654f4a22012-02-09 14:15:44 -0500369 def authorize(self, http):
370 """Authorize an httplib2.Http instance with these credentials.
371
372 The modified http.request method will add authentication headers to each
373 request and will refresh access_tokens when a 401 is received on a
374 request. In addition the http.request method has a credentials property,
375 http.request.credentials, which is the Credentials object that authorized
376 it.
377
378 Args:
379 http: An instance of httplib2.Http
380 or something that acts like it.
381
382 Returns:
383 A modified instance of http that was passed in.
384
385 Example:
386
387 h = httplib2.Http()
388 h = credentials.authorize(h)
389
390 You can't create a new OAuth subclass of httplib2.Authenication
391 because it never gets passed the absolute URI, which is needed for
392 signing. So instead we have to overload 'request' with a closure
393 that adds in the Authorization header and then calls the original
394 version of 'request()'.
395 """
396 request_orig = http.request
397
398 # The closure that will replace 'httplib2.Http.request'.
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400399 @util.positional(1)
Joe Gregorio654f4a22012-02-09 14:15:44 -0500400 def new_request(uri, method='GET', body=None, headers=None,
401 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
402 connection_type=None):
403 if not self.access_token:
404 logger.info('Attempting refresh to obtain initial access_token')
405 self._refresh(request_orig)
406
407 # Modify the request headers to add the appropriate
408 # Authorization header.
409 if headers is None:
410 headers = {}
411 self.apply(headers)
412
413 if self.user_agent is not None:
414 if 'user-agent' in headers:
415 headers['user-agent'] = self.user_agent + ' ' + headers['user-agent']
416 else:
417 headers['user-agent'] = self.user_agent
418
419 resp, content = request_orig(uri, method, body, headers,
420 redirections, connection_type)
421
Joe Gregorio7c7c6b12012-07-16 16:31:01 -0400422 # Older API (GData) respond with 403
423 if resp.status in [401, 403]:
424 logger.info('Refreshing due to a %s' % str(resp.status))
Joe Gregorio654f4a22012-02-09 14:15:44 -0500425 self._refresh(request_orig)
426 self.apply(headers)
427 return request_orig(uri, method, body, headers,
428 redirections, connection_type)
429 else:
430 return (resp, content)
431
432 # Replace the request method with our own closure.
433 http.request = new_request
434
435 # Set credentials as a property of the request method.
436 setattr(http.request, 'credentials', self)
437
438 return http
439
440 def refresh(self, http):
441 """Forces a refresh of the access_token.
442
443 Args:
444 http: httplib2.Http, an http object to be used to make the refresh
445 request.
446 """
447 self._refresh(http.request)
448
449 def apply(self, headers):
450 """Add the authorization to the headers.
451
452 Args:
453 headers: dict, the headers to add the Authorization header to.
454 """
455 headers['Authorization'] = 'Bearer ' + self.access_token
456
Joe Gregorio562b7312011-09-15 09:06:38 -0400457 def to_json(self):
458 return self._to_json(Credentials.NON_SERIALIZED_MEMBERS)
459
460 @classmethod
461 def from_json(cls, s):
462 """Instantiate a Credentials object from a JSON description of it. The JSON
463 should have been produced by calling .to_json() on the object.
464
465 Args:
466 data: dict, A deserialized JSON object.
467
468 Returns:
469 An instance of a Credentials subclass.
470 """
471 data = simplejson.loads(s)
472 if 'token_expiry' in data and not isinstance(data['token_expiry'],
473 datetime.datetime):
Joe Gregorio1daa71b2011-09-15 18:12:14 -0400474 try:
475 data['token_expiry'] = datetime.datetime.strptime(
476 data['token_expiry'], EXPIRY_FORMAT)
477 except:
478 data['token_expiry'] = None
Joe Gregorio562b7312011-09-15 09:06:38 -0400479 retval = OAuth2Credentials(
480 data['access_token'],
481 data['client_id'],
482 data['client_secret'],
483 data['refresh_token'],
484 data['token_expiry'],
485 data['token_uri'],
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500486 data['user_agent'],
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400487 id_token=data.get('id_token', None))
Joe Gregorio562b7312011-09-15 09:06:38 -0400488 retval.invalid = data['invalid']
489 return retval
490
Joe Gregorio9ce4b622011-02-17 15:32:11 -0500491 @property
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400492 def access_token_expired(self):
493 """True if the credential is expired or invalid.
494
495 If the token_expiry isn't set, we assume the token doesn't expire.
496 """
497 if self.invalid:
498 return True
499
500 if not self.token_expiry:
501 return False
502
Joe Gregorio562b7312011-09-15 09:06:38 -0400503 now = datetime.datetime.utcnow()
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400504 if now >= self.token_expiry:
505 logger.info('access_token is expired. Now: %s, token_expiry: %s',
506 now, self.token_expiry)
507 return True
508 return False
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500509
Joe Gregorio695fdc12011-01-16 16:46:55 -0500510 def set_store(self, store):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400511 """Set the Storage for the credential.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500512
513 Args:
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400514 store: Storage, an implementation of Stroage object.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500515 This is needed to store the latest access_token if it
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400516 has expired and been refreshed. This implementation uses
517 locking to check for updates before updating the
518 access_token.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500519 """
520 self.store = store
521
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400522 def _updateFromCredential(self, other):
523 """Update this Credential from another instance."""
524 self.__dict__.update(other.__getstate__())
525
Joe Gregorio695fdc12011-01-16 16:46:55 -0500526 def __getstate__(self):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400527 """Trim the state down to something that can be pickled."""
Joe Gregorio695fdc12011-01-16 16:46:55 -0500528 d = copy.copy(self.__dict__)
529 del d['store']
530 return d
531
532 def __setstate__(self, state):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400533 """Reconstitute the state of the object from being pickled."""
Joe Gregorio695fdc12011-01-16 16:46:55 -0500534 self.__dict__.update(state)
535 self.store = None
536
JacobMoshenko8e905102011-06-20 09:53:10 -0400537 def _generate_refresh_request_body(self):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400538 """Generate the body that will be used in the refresh request."""
JacobMoshenko8e905102011-06-20 09:53:10 -0400539 body = urllib.urlencode({
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400540 'grant_type': 'refresh_token',
541 'client_id': self.client_id,
542 'client_secret': self.client_secret,
543 'refresh_token': self.refresh_token,
544 })
JacobMoshenko8e905102011-06-20 09:53:10 -0400545 return body
546
547 def _generate_refresh_request_headers(self):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400548 """Generate the headers that will be used in the refresh request."""
JacobMoshenko8e905102011-06-20 09:53:10 -0400549 headers = {
JacobMoshenko8e905102011-06-20 09:53:10 -0400550 'content-type': 'application/x-www-form-urlencoded',
551 }
JacobMoshenkocb6d8912011-07-08 13:35:15 -0400552
553 if self.user_agent is not None:
554 headers['user-agent'] = self.user_agent
555
JacobMoshenko8e905102011-06-20 09:53:10 -0400556 return headers
557
Joe Gregorio695fdc12011-01-16 16:46:55 -0500558 def _refresh(self, http_request):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400559 """Refreshes the access_token.
560
561 This method first checks by reading the Storage object if available.
562 If a refresh is still needed, it holds the Storage lock until the
563 refresh is completed.
Joe Gregorio654f4a22012-02-09 14:15:44 -0500564
565 Args:
566 http_request: callable, a callable that matches the method signature of
567 httplib2.Http.request, used to make the refresh request.
568
569 Raises:
570 AccessTokenRefreshError: When the refresh fails.
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400571 """
572 if not self.store:
573 self._do_refresh_request(http_request)
574 else:
575 self.store.acquire_lock()
576 try:
577 new_cred = self.store.locked_get()
578 if (new_cred and not new_cred.invalid and
579 new_cred.access_token != self.access_token):
580 logger.info('Updated access_token read from Storage')
581 self._updateFromCredential(new_cred)
582 else:
583 self._do_refresh_request(http_request)
584 finally:
585 self.store.release_lock()
586
587 def _do_refresh_request(self, http_request):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500588 """Refresh the access_token using the refresh_token.
589
590 Args:
Joe Gregorio654f4a22012-02-09 14:15:44 -0500591 http_request: callable, a callable that matches the method signature of
592 httplib2.Http.request, used to make the refresh request.
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400593
594 Raises:
595 AccessTokenRefreshError: When the refresh fails.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500596 """
JacobMoshenko8e905102011-06-20 09:53:10 -0400597 body = self._generate_refresh_request_body()
598 headers = self._generate_refresh_request_headers()
599
Joe Gregorio4b4002f2012-06-14 15:41:01 -0400600 logger.info('Refreshing access_token')
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500601 resp, content = http_request(
602 self.token_uri, method='POST', body=body, headers=headers)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500603 if resp.status == 200:
604 # TODO(jcgregorio) Raise an error if loads fails?
605 d = simplejson.loads(content)
606 self.access_token = d['access_token']
607 self.refresh_token = d.get('refresh_token', self.refresh_token)
608 if 'expires_in' in d:
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500609 self.token_expiry = datetime.timedelta(
Joe Gregorio562b7312011-09-15 09:06:38 -0400610 seconds=int(d['expires_in'])) + datetime.datetime.utcnow()
Joe Gregorio695fdc12011-01-16 16:46:55 -0500611 else:
612 self.token_expiry = None
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400613 if self.store:
614 self.store.locked_put(self)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500615 else:
JacobMoshenko8e905102011-06-20 09:53:10 -0400616 # An {'error':...} response body means the token is expired or revoked,
617 # so we flag the credentials as such.
Joe Gregorioe78621a2012-03-09 15:47:23 -0500618 logger.info('Failed to retrieve access token: %s' % content)
Joe Gregorioccc79542011-02-19 00:05:26 -0500619 error_msg = 'Invalid response %s.' % resp['status']
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500620 try:
621 d = simplejson.loads(content)
622 if 'error' in d:
Joe Gregorioccc79542011-02-19 00:05:26 -0500623 error_msg = d['error']
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400624 self.invalid = True
625 if self.store:
626 self.store.locked_put(self)
Joe Gregoriofd08e432012-08-09 14:17:41 -0400627 except StandardError:
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500628 pass
Joe Gregorioccc79542011-02-19 00:05:26 -0500629 raise AccessTokenRefreshError(error_msg)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500630
Joe Gregorio695fdc12011-01-16 16:46:55 -0500631
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500632class AccessTokenCredentials(OAuth2Credentials):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400633 """Credentials object for OAuth 2.0.
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500634
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400635 Credentials can be applied to an httplib2.Http object using the
636 authorize() method, which then signs each request from that object
637 with the OAuth 2.0 access token. This set of credentials is for the
638 use case where you have acquired an OAuth 2.0 access_token from
639 another place such as a JavaScript client or another web
640 application, and wish to use it from Python. Because only the
641 access_token is present it can not be refreshed and will in time
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500642 expire.
643
Joe Gregorio9ce4b622011-02-17 15:32:11 -0500644 AccessTokenCredentials objects may be safely pickled and unpickled.
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500645
646 Usage:
647 credentials = AccessTokenCredentials('<an access token>',
648 'my-user-agent/1.0')
649 http = httplib2.Http()
650 http = credentials.authorize(http)
651
652 Exceptions:
653 AccessTokenCredentialsExpired: raised when the access_token expires or is
654 revoked.
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500655 """
656
657 def __init__(self, access_token, user_agent):
658 """Create an instance of OAuth2Credentials
659
660 This is one of the few types if Credentials that you should contrust,
661 Credentials objects are usually instantiated by a Flow.
662
663 Args:
ade@google.com93a7f7c2011-02-23 16:00:37 +0000664 access_token: string, access token.
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500665 user_agent: string, The HTTP User-Agent to provide for this application.
666
667 Notes:
668 store: callable, a callable that when passed a Credential
669 will store the credential back to where it came from.
670 """
671 super(AccessTokenCredentials, self).__init__(
672 access_token,
673 None,
674 None,
675 None,
676 None,
677 None,
678 user_agent)
679
Joe Gregorio562b7312011-09-15 09:06:38 -0400680
681 @classmethod
682 def from_json(cls, s):
683 data = simplejson.loads(s)
684 retval = AccessTokenCredentials(
685 data['access_token'],
686 data['user_agent'])
687 return retval
688
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500689 def _refresh(self, http_request):
690 raise AccessTokenCredentialsError(
691 "The access_token is expired or invalid and can't be refreshed.")
692
JacobMoshenko8e905102011-06-20 09:53:10 -0400693
694class AssertionCredentials(OAuth2Credentials):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400695 """Abstract Credentials object used for OAuth 2.0 assertion grants.
JacobMoshenko8e905102011-06-20 09:53:10 -0400696
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400697 This credential does not require a flow to instantiate because it
698 represents a two legged flow, and therefore has all of the required
699 information to generate and refresh its own access tokens. It must
700 be subclassed to generate the appropriate assertion string.
JacobMoshenko8e905102011-06-20 09:53:10 -0400701
702 AssertionCredentials objects may be safely pickled and unpickled.
703 """
704
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400705 @util.positional(2)
706 def __init__(self, assertion_type, user_agent=None,
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400707 token_uri='https://accounts.google.com/o/oauth2/token',
708 **unused_kwargs):
709 """Constructor for AssertionFlowCredentials.
JacobMoshenko8e905102011-06-20 09:53:10 -0400710
711 Args:
712 assertion_type: string, assertion type that will be declared to the auth
713 server
714 user_agent: string, The HTTP User-Agent to provide for this application.
715 token_uri: string, URI for token endpoint. For convenience
716 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
717 """
718 super(AssertionCredentials, self).__init__(
719 None,
720 None,
721 None,
722 None,
723 None,
724 token_uri,
725 user_agent)
726 self.assertion_type = assertion_type
727
728 def _generate_refresh_request_body(self):
729 assertion = self._generate_assertion()
730
731 body = urllib.urlencode({
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400732 'assertion_type': self.assertion_type,
733 'assertion': assertion,
734 'grant_type': 'assertion',
735 })
JacobMoshenko8e905102011-06-20 09:53:10 -0400736
737 return body
738
739 def _generate_assertion(self):
740 """Generate the assertion string that will be used in the access token
741 request.
742 """
743 _abstract()
744
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500745if HAS_OPENSSL:
746 # PyOpenSSL is not a prerequisite for oauth2client, so if it is missing then
747 # don't create the SignedJwtAssertionCredentials or the verify_id_token()
748 # method.
749
750 class SignedJwtAssertionCredentials(AssertionCredentials):
751 """Credentials object used for OAuth 2.0 Signed JWT assertion grants.
752
Joe Gregorio672051e2012-07-10 09:11:45 -0400753 This credential does not require a flow to instantiate because it represents
754 a two legged flow, and therefore has all of the required information to
755 generate and refresh its own access tokens.
756
757 SignedJwtAssertionCredentials requires PyOpenSSL and because of that it does
758 not work on App Engine. For App Engine you may consider using
759 AppAssertionCredentials.
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500760 """
761
762 MAX_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds
763
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400764 @util.positional(4)
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500765 def __init__(self,
766 service_account_name,
767 private_key,
768 scope,
769 private_key_password='notasecret',
770 user_agent=None,
771 token_uri='https://accounts.google.com/o/oauth2/token',
772 **kwargs):
773 """Constructor for SignedJwtAssertionCredentials.
774
775 Args:
776 service_account_name: string, id for account, usually an email address.
777 private_key: string, private key in P12 format.
Joe Gregorio5cf5d122012-11-16 16:36:12 -0500778 scope: string or iterable of strings, scope(s) of the credentials being
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500779 requested.
780 private_key_password: string, password for private_key.
781 user_agent: string, HTTP User-Agent to provide for this application.
782 token_uri: string, URI for token endpoint. For convenience
783 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
784 kwargs: kwargs, Additional parameters to add to the JWT token, for
785 example prn=joe@xample.org."""
786
787 super(SignedJwtAssertionCredentials, self).__init__(
788 'http://oauth.net/grant_type/jwt/1.0/bearer',
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400789 user_agent=user_agent,
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500790 token_uri=token_uri,
791 )
792
Joe Gregorio5cf5d122012-11-16 16:36:12 -0500793 self.scope = util.scopes_to_string(scope)
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500794
Joe Gregorioa19f3a72012-07-11 15:35:35 -0400795 # Keep base64 encoded so it can be stored in JSON.
796 self.private_key = base64.b64encode(private_key)
797
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500798 self.private_key_password = private_key_password
799 self.service_account_name = service_account_name
800 self.kwargs = kwargs
801
802 @classmethod
803 def from_json(cls, s):
804 data = simplejson.loads(s)
805 retval = SignedJwtAssertionCredentials(
806 data['service_account_name'],
Joe Gregorioa19f3a72012-07-11 15:35:35 -0400807 base64.b64decode(data['private_key']),
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500808 data['scope'],
Joe Gregorioa19f3a72012-07-11 15:35:35 -0400809 private_key_password=data['private_key_password'],
810 user_agent=data['user_agent'],
811 token_uri=data['token_uri'],
812 **data['kwargs']
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500813 )
814 retval.invalid = data['invalid']
Joe Gregorioa19f3a72012-07-11 15:35:35 -0400815 retval.access_token = data['access_token']
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500816 return retval
817
818 def _generate_assertion(self):
819 """Generate the assertion that will be used in the request."""
820 now = long(time.time())
821 payload = {
822 'aud': self.token_uri,
823 'scope': self.scope,
824 'iat': now,
825 'exp': now + SignedJwtAssertionCredentials.MAX_TOKEN_LIFETIME_SECS,
826 'iss': self.service_account_name
827 }
828 payload.update(self.kwargs)
Joe Gregorioe78621a2012-03-09 15:47:23 -0500829 logger.debug(str(payload))
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500830
Joe Gregorioa19f3a72012-07-11 15:35:35 -0400831 private_key = base64.b64decode(self.private_key)
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500832 return make_signed_jwt(
Joe Gregorioa19f3a72012-07-11 15:35:35 -0400833 Signer.from_string(private_key, self.private_key_password), payload)
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500834
Joe Gregorio9f2f38f2012-02-06 12:53:00 -0500835 # Only used in verify_id_token(), which is always calling to the same URI
836 # for the certs.
837 _cached_http = httplib2.Http(MemoryCache())
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500838
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400839 @util.positional(2)
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500840 def verify_id_token(id_token, audience, http=None,
841 cert_uri=ID_TOKEN_VERIFICATON_CERTS):
842 """Verifies a signed JWT id_token.
843
Joe Gregorio672051e2012-07-10 09:11:45 -0400844 This function requires PyOpenSSL and because of that it does not work on
845 App Engine. For App Engine you may consider using AppAssertionCredentials.
846
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500847 Args:
848 id_token: string, A Signed JWT.
849 audience: string, The audience 'aud' that the token should be for.
850 http: httplib2.Http, instance to use to make the HTTP request. Callers
851 should supply an instance that has caching enabled.
852 cert_uri: string, URI of the certificates in JSON format to
853 verify the JWT against.
854
855 Returns:
856 The deserialized JSON in the JWT.
857
858 Raises:
859 oauth2client.crypt.AppIdentityError if the JWT fails to verify.
860 """
861 if http is None:
Joe Gregorio9f2f38f2012-02-06 12:53:00 -0500862 http = _cached_http
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500863
864 resp, content = http.request(cert_uri)
865
866 if resp.status == 200:
867 certs = simplejson.loads(content)
868 return verify_signed_jwt_with_certs(id_token, certs, audience)
869 else:
870 raise VerifyJwtTokenError('Status code: %d' % resp.status)
871
872
873def _urlsafe_b64decode(b64string):
Joe Gregoriobd512b52011-12-06 15:39:26 -0500874 # Guard against unicode strings, which base64 can't handle.
875 b64string = b64string.encode('ascii')
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500876 padded = b64string + '=' * (4 - len(b64string) % 4)
877 return base64.urlsafe_b64decode(padded)
878
879
880def _extract_id_token(id_token):
881 """Extract the JSON payload from a JWT.
882
883 Does the extraction w/o checking the signature.
884
885 Args:
886 id_token: string, OAuth 2.0 id_token.
887
888 Returns:
889 object, The deserialized JSON payload.
890 """
891 segments = id_token.split('.')
892
893 if (len(segments) != 3):
894 raise VerifyJwtTokenError(
895 'Wrong number of segments in token: %s' % id_token)
896
897 return simplejson.loads(_urlsafe_b64decode(segments[1]))
898
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400899
Joe Gregorioddb969a2012-07-11 11:04:12 -0400900def _parse_exchange_token_response(content):
901 """Parses response of an exchange token request.
902
903 Most providers return JSON but some (e.g. Facebook) return a
904 url-encoded string.
905
906 Args:
907 content: The body of a response
908
909 Returns:
910 Content as a dictionary object. Note that the dict could be empty,
911 i.e. {}. That basically indicates a failure.
912 """
913 resp = {}
914 try:
915 resp = simplejson.loads(content)
916 except StandardError:
917 # different JSON libs raise different exceptions,
918 # so we just do a catch-all here
919 resp = dict(parse_qsl(content))
920
921 # some providers respond with 'expires', others with 'expires_in'
922 if resp and 'expires' in resp:
923 resp['expires_in'] = resp.pop('expires')
924
925 return resp
926
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400927
928@util.positional(4)
Joe Gregorio32d852d2012-06-14 09:08:18 -0400929def credentials_from_code(client_id, client_secret, scope, code,
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400930 redirect_uri='postmessage', http=None, user_agent=None,
931 token_uri='https://accounts.google.com/o/oauth2/token'):
Joe Gregorio32d852d2012-06-14 09:08:18 -0400932 """Exchanges an authorization code for an OAuth2Credentials object.
933
934 Args:
935 client_id: string, client identifier.
936 client_secret: string, client secret.
Joe Gregorio5cf5d122012-11-16 16:36:12 -0500937 scope: string or iterable of strings, scope(s) to request.
Joe Gregorio32d852d2012-06-14 09:08:18 -0400938 code: string, An authroization code, most likely passed down from
939 the client
940 redirect_uri: string, this is generally set to 'postmessage' to match the
941 redirect_uri that the client specified
942 http: httplib2.Http, optional http instance to use to do the fetch
943 token_uri: string, URI for token endpoint. For convenience
944 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
945 Returns:
946 An OAuth2Credentials object.
947
948 Raises:
949 FlowExchangeError if the authorization code cannot be exchanged for an
950 access token
951 """
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400952 flow = OAuth2WebServerFlow(client_id, client_secret, scope,
953 redirect_uri=redirect_uri, user_agent=user_agent,
954 auth_uri='https://accounts.google.com/o/oauth2/auth',
955 token_uri=token_uri)
Joe Gregorio32d852d2012-06-14 09:08:18 -0400956
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400957 credentials = flow.step2_exchange(code, http=http)
Joe Gregorio32d852d2012-06-14 09:08:18 -0400958 return credentials
959
960
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400961@util.positional(3)
Joe Gregorio32d852d2012-06-14 09:08:18 -0400962def credentials_from_clientsecrets_and_code(filename, scope, code,
963 message = None,
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400964 redirect_uri='postmessage',
Joe Gregorioc29aaa92012-07-16 16:16:31 -0400965 http=None,
966 cache=None):
Joe Gregorio32d852d2012-06-14 09:08:18 -0400967 """Returns OAuth2Credentials from a clientsecrets file and an auth code.
968
969 Will create the right kind of Flow based on the contents of the clientsecrets
970 file or will raise InvalidClientSecretsError for unknown types of Flows.
971
972 Args:
973 filename: string, File name of clientsecrets.
Joe Gregorio5cf5d122012-11-16 16:36:12 -0500974 scope: string or iterable of strings, scope(s) to request.
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400975 code: string, An authorization code, most likely passed down from
Joe Gregorio32d852d2012-06-14 09:08:18 -0400976 the client
977 message: string, A friendly string to display to the user if the
978 clientsecrets file is missing or invalid. If message is provided then
979 sys.exit will be called in the case of an error. If message in not
980 provided then clientsecrets.InvalidClientSecretsError will be raised.
981 redirect_uri: string, this is generally set to 'postmessage' to match the
982 redirect_uri that the client specified
983 http: httplib2.Http, optional http instance to use to do the fetch
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400984 cache: An optional cache service client that implements get() and set()
Joe Gregorioc29aaa92012-07-16 16:16:31 -0400985 methods. See clientsecrets.loadfile() for details.
Joe Gregorio32d852d2012-06-14 09:08:18 -0400986
987 Returns:
988 An OAuth2Credentials object.
989
990 Raises:
991 FlowExchangeError if the authorization code cannot be exchanged for an
992 access token
993 UnknownClientSecretsFlowError if the file describes an unknown kind of Flow.
994 clientsecrets.InvalidClientSecretsError if the clientsecrets file is
995 invalid.
996 """
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400997 flow = flow_from_clientsecrets(filename, scope, message=message, cache=cache,
998 redirect_uri=redirect_uri)
999 credentials = flow.step2_exchange(code, http=http)
Joe Gregorio32d852d2012-06-14 09:08:18 -04001000 return credentials
1001
JacobMoshenko8e905102011-06-20 09:53:10 -04001002
Joe Gregorio695fdc12011-01-16 16:46:55 -05001003class OAuth2WebServerFlow(Flow):
1004 """Does the Web Server Flow for OAuth 2.0.
1005
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001006 OAuth2WebServerFlow objects may be safely pickled and unpickled.
Joe Gregorio695fdc12011-01-16 16:46:55 -05001007 """
1008
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001009 @util.positional(4)
1010 def __init__(self, client_id, client_secret, scope,
1011 redirect_uri=None,
1012 user_agent=None,
Joe Gregorio9da2ad82011-09-11 14:04:44 -04001013 auth_uri='https://accounts.google.com/o/oauth2/auth',
1014 token_uri='https://accounts.google.com/o/oauth2/token',
1015 **kwargs):
1016 """Constructor for OAuth2WebServerFlow.
Joe Gregorio695fdc12011-01-16 16:46:55 -05001017
Joe Gregorio32f73192012-10-23 16:13:44 -04001018 The kwargs argument is used to set extra query parameters on the
1019 auth_uri. For example, the access_type and approval_prompt
1020 query parameters can be set via kwargs.
1021
Joe Gregorio695fdc12011-01-16 16:46:55 -05001022 Args:
Joe Gregorio7c22ab22011-02-16 15:32:39 -05001023 client_id: string, client identifier.
1024 client_secret: string client secret.
Joe Gregorio5cf5d122012-11-16 16:36:12 -05001025 scope: string or iterable of strings, scope(s) of the credentials being
Joe Gregoriof2f8a5a2011-10-14 15:11:29 -04001026 requested.
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001027 redirect_uri: string, Either the string 'urn:ietf:wg:oauth:2.0:oob' for
1028 a non-web-based application, or a URI that handles the callback from
1029 the authorization server.
Joe Gregorio695fdc12011-01-16 16:46:55 -05001030 user_agent: string, HTTP User-Agent to provide for this application.
Joe Gregorio7c22ab22011-02-16 15:32:39 -05001031 auth_uri: string, URI for authorization endpoint. For convenience
1032 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
1033 token_uri: string, URI for token endpoint. For convenience
1034 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
Joe Gregorio695fdc12011-01-16 16:46:55 -05001035 **kwargs: dict, The keyword arguments are all optional and required
1036 parameters for the OAuth calls.
1037 """
1038 self.client_id = client_id
1039 self.client_secret = client_secret
Joe Gregorio5cf5d122012-11-16 16:36:12 -05001040 self.scope = util.scopes_to_string(scope)
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001041 self.redirect_uri = redirect_uri
Joe Gregorio695fdc12011-01-16 16:46:55 -05001042 self.user_agent = user_agent
Joe Gregorio7c22ab22011-02-16 15:32:39 -05001043 self.auth_uri = auth_uri
Joe Gregorio695fdc12011-01-16 16:46:55 -05001044 self.token_uri = token_uri
Joe Gregorio69a0aca2011-11-03 10:47:32 -04001045 self.params = {
1046 'access_type': 'offline',
Joe Gregorio32f73192012-10-23 16:13:44 -04001047 'response_type': 'code',
Joe Gregorio69a0aca2011-11-03 10:47:32 -04001048 }
1049 self.params.update(kwargs)
Joe Gregorio695fdc12011-01-16 16:46:55 -05001050
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001051 @util.positional(1)
1052 def step1_get_authorize_url(self, redirect_uri=None):
Joe Gregorio695fdc12011-01-16 16:46:55 -05001053 """Returns a URI to redirect to the provider.
1054
1055 Args:
Joe Gregoriof2326c02012-02-09 12:18:44 -05001056 redirect_uri: string, Either the string 'urn:ietf:wg:oauth:2.0:oob' for
1057 a non-web-based application, or a URI that handles the callback from
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001058 the authorization server. This parameter is deprecated, please move to
1059 passing the redirect_uri in via the constructor.
Joe Gregorio695fdc12011-01-16 16:46:55 -05001060
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001061 Returns:
1062 A URI as a string to redirect the user to begin the authorization flow.
Joe Gregorio695fdc12011-01-16 16:46:55 -05001063 """
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001064 if redirect_uri is not None:
1065 logger.warning(('The redirect_uri parameter for'
1066 'OAuth2WebServerFlow.step1_get_authorize_url is deprecated. Please'
1067 'move to passing the redirect_uri in via the constructor.'))
1068 self.redirect_uri = redirect_uri
Joe Gregorio695fdc12011-01-16 16:46:55 -05001069
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001070 if self.redirect_uri is None:
1071 raise ValueError('The value of redirect_uri must not be None.')
1072
Joe Gregorio695fdc12011-01-16 16:46:55 -05001073 query = {
Joe Gregorio9da2ad82011-09-11 14:04:44 -04001074 'client_id': self.client_id,
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001075 'redirect_uri': self.redirect_uri,
Joe Gregorio9da2ad82011-09-11 14:04:44 -04001076 'scope': self.scope,
1077 }
Joe Gregorio695fdc12011-01-16 16:46:55 -05001078 query.update(self.params)
Joe Gregorio7c22ab22011-02-16 15:32:39 -05001079 parts = list(urlparse.urlparse(self.auth_uri))
Joe Gregorio695fdc12011-01-16 16:46:55 -05001080 query.update(dict(parse_qsl(parts[4]))) # 4 is the index of the query part
1081 parts[4] = urllib.urlencode(query)
1082 return urlparse.urlunparse(parts)
1083
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001084 @util.positional(2)
Joe Gregorioccc79542011-02-19 00:05:26 -05001085 def step2_exchange(self, code, http=None):
Joe Gregorio695fdc12011-01-16 16:46:55 -05001086 """Exhanges a code for OAuth2Credentials.
1087
1088 Args:
1089 code: string or dict, either the code as a string, or a dictionary
1090 of the query parameters to the redirect_uri, which contains
1091 the code.
Joe Gregorioccc79542011-02-19 00:05:26 -05001092 http: httplib2.Http, optional http instance to use to do the fetch
Joe Gregorio4b4002f2012-06-14 15:41:01 -04001093
1094 Returns:
1095 An OAuth2Credentials object that can be used to authorize requests.
1096
1097 Raises:
1098 FlowExchangeError if a problem occured exchanging the code for a
1099 refresh_token.
Joe Gregorio695fdc12011-01-16 16:46:55 -05001100 """
1101
1102 if not (isinstance(code, str) or isinstance(code, unicode)):
Joe Gregorio4b4002f2012-06-14 15:41:01 -04001103 if 'code' not in code:
1104 if 'error' in code:
1105 error_msg = code['error']
1106 else:
1107 error_msg = 'No code was supplied in the query parameters.'
1108 raise FlowExchangeError(error_msg)
1109 else:
1110 code = code['code']
Joe Gregorio695fdc12011-01-16 16:46:55 -05001111
1112 body = urllib.urlencode({
Joe Gregorio9da2ad82011-09-11 14:04:44 -04001113 'grant_type': 'authorization_code',
1114 'client_id': self.client_id,
1115 'client_secret': self.client_secret,
1116 'code': code,
1117 'redirect_uri': self.redirect_uri,
1118 'scope': self.scope,
1119 })
Joe Gregorio695fdc12011-01-16 16:46:55 -05001120 headers = {
Joe Gregorio9da2ad82011-09-11 14:04:44 -04001121 'content-type': 'application/x-www-form-urlencoded',
Joe Gregorio695fdc12011-01-16 16:46:55 -05001122 }
JacobMoshenkocb6d8912011-07-08 13:35:15 -04001123
1124 if self.user_agent is not None:
1125 headers['user-agent'] = self.user_agent
1126
Joe Gregorioccc79542011-02-19 00:05:26 -05001127 if http is None:
1128 http = httplib2.Http()
Joe Gregorio8b4c1732011-12-06 11:28:29 -05001129
JacobMoshenko8e905102011-06-20 09:53:10 -04001130 resp, content = http.request(self.token_uri, method='POST', body=body,
1131 headers=headers)
Joe Gregorioddb969a2012-07-11 11:04:12 -04001132 d = _parse_exchange_token_response(content)
1133 if resp.status == 200 and 'access_token' in d:
Joe Gregorio695fdc12011-01-16 16:46:55 -05001134 access_token = d['access_token']
1135 refresh_token = d.get('refresh_token', None)
1136 token_expiry = None
1137 if 'expires_in' in d:
Joe Gregorio562b7312011-09-15 09:06:38 -04001138 token_expiry = datetime.datetime.utcnow() + datetime.timedelta(
JacobMoshenko8e905102011-06-20 09:53:10 -04001139 seconds=int(d['expires_in']))
Joe Gregorio695fdc12011-01-16 16:46:55 -05001140
Joe Gregorio8b4c1732011-12-06 11:28:29 -05001141 if 'id_token' in d:
1142 d['id_token'] = _extract_id_token(d['id_token'])
1143
Joe Gregorio6ceea2d2012-08-24 11:57:58 -04001144 logger.info('Successfully retrieved access token')
JacobMoshenko8e905102011-06-20 09:53:10 -04001145 return OAuth2Credentials(access_token, self.client_id,
1146 self.client_secret, refresh_token, token_expiry,
Joe Gregorio8b4c1732011-12-06 11:28:29 -05001147 self.token_uri, self.user_agent,
1148 id_token=d.get('id_token', None))
Joe Gregorio695fdc12011-01-16 16:46:55 -05001149 else:
Joe Gregorioe78621a2012-03-09 15:47:23 -05001150 logger.info('Failed to retrieve access token: %s' % content)
Joe Gregorioddb969a2012-07-11 11:04:12 -04001151 if 'error' in d:
1152 # you never know what those providers got to say
1153 error_msg = unicode(d['error'])
1154 else:
1155 error_msg = 'Invalid response: %s.' % str(resp.status)
Joe Gregorioccc79542011-02-19 00:05:26 -05001156 raise FlowExchangeError(error_msg)
Joe Gregoriof08a4982011-10-07 13:11:16 -04001157
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001158
1159@util.positional(2)
1160def flow_from_clientsecrets(filename, scope, redirect_uri=None, message=None, cache=None):
Joe Gregoriof08a4982011-10-07 13:11:16 -04001161 """Create a Flow from a clientsecrets file.
1162
1163 Will create the right kind of Flow based on the contents of the clientsecrets
1164 file or will raise InvalidClientSecretsError for unknown types of Flows.
1165
1166 Args:
1167 filename: string, File name of client secrets.
Joe Gregorio5cf5d122012-11-16 16:36:12 -05001168 scope: string or iterable of strings, scope(s) to request.
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001169 redirect_uri: string, Either the string 'urn:ietf:wg:oauth:2.0:oob' for
1170 a non-web-based application, or a URI that handles the callback from
1171 the authorization server.
Joe Gregoriof08a4982011-10-07 13:11:16 -04001172 message: string, A friendly string to display to the user if the
1173 clientsecrets file is missing or invalid. If message is provided then
1174 sys.exit will be called in the case of an error. If message in not
1175 provided then clientsecrets.InvalidClientSecretsError will be raised.
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001176 cache: An optional cache service client that implements get() and set()
Joe Gregorioc29aaa92012-07-16 16:16:31 -04001177 methods. See clientsecrets.loadfile() for details.
Joe Gregoriof08a4982011-10-07 13:11:16 -04001178
1179 Returns:
1180 A Flow object.
1181
1182 Raises:
1183 UnknownClientSecretsFlowError if the file describes an unknown kind of Flow.
1184 clientsecrets.InvalidClientSecretsError if the clientsecrets file is
1185 invalid.
1186 """
Joe Gregorio0984ef22011-10-14 13:17:43 -04001187 try:
Joe Gregorioc29aaa92012-07-16 16:16:31 -04001188 client_type, client_info = clientsecrets.loadfile(filename, cache=cache)
Joe Gregorio0984ef22011-10-14 13:17:43 -04001189 if client_type in [clientsecrets.TYPE_WEB, clientsecrets.TYPE_INSTALLED]:
1190 return OAuth2WebServerFlow(
1191 client_info['client_id'],
1192 client_info['client_secret'],
1193 scope,
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001194 redirect_uri=redirect_uri,
1195 user_agent=None,
1196 auth_uri=client_info['auth_uri'],
1197 token_uri=client_info['token_uri'])
1198
Joe Gregorio0984ef22011-10-14 13:17:43 -04001199 except clientsecrets.InvalidClientSecretsError:
1200 if message:
1201 sys.exit(message)
1202 else:
1203 raise
Joe Gregoriof08a4982011-10-07 13:11:16 -04001204 else:
1205 raise UnknownClientSecretsFlowError(
1206 'This OAuth 2.0 flow is unsupported: "%s"' * client_type)