blob: 301d1ed0cd347079b5bb414d4c9784fbb15e9e7c [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
Joe Gregorio0b723c22013-01-03 15:00:50 -050037HAS_CRYPTO = False
Joe Gregorio8b4c1732011-12-06 11:28:29 -050038try:
Joe Gregorio0b723c22013-01-03 15:00:50 -050039 from oauth2client import crypt
40 HAS_CRYPTO = True
Joe Gregorio8b4c1732011-12-06 11:28:29 -050041except ImportError:
42 pass
43
Joe Gregorio695fdc12011-01-16 16:46:55 -050044try:
Joe Gregorio9da2ad82011-09-11 14:04:44 -040045 from urlparse import parse_qsl
Joe Gregorio695fdc12011-01-16 16:46:55 -050046except ImportError:
Joe Gregorio9da2ad82011-09-11 14:04:44 -040047 from cgi import parse_qsl
48
49logger = logging.getLogger(__name__)
Joe Gregorio695fdc12011-01-16 16:46:55 -050050
Joe Gregorio562b7312011-09-15 09:06:38 -040051# Expiry is stored in RFC3339 UTC format
Joe Gregorio8b4c1732011-12-06 11:28:29 -050052EXPIRY_FORMAT = '%Y-%m-%dT%H:%M:%SZ'
53
54# Which certs to use to validate id_tokens received.
55ID_TOKEN_VERIFICATON_CERTS = 'https://www.googleapis.com/oauth2/v1/certs'
Joe Gregorio562b7312011-09-15 09:06:38 -040056
Joe Gregoriof2326c02012-02-09 12:18:44 -050057# Constant to use for the out of band OAuth 2.0 flow.
58OOB_CALLBACK_URN = 'urn:ietf:wg:oauth:2.0:oob'
59
Joe Gregorio695fdc12011-01-16 16:46:55 -050060
61class Error(Exception):
62 """Base error for this module."""
63 pass
64
65
Joe Gregorioccc79542011-02-19 00:05:26 -050066class FlowExchangeError(Error):
Joe Gregorioca876e42011-02-22 19:39:42 -050067 """Error trying to exchange an authorization grant for an access token."""
Joe Gregorioccc79542011-02-19 00:05:26 -050068 pass
69
70
71class AccessTokenRefreshError(Error):
Joe Gregorioca876e42011-02-22 19:39:42 -050072 """Error trying to refresh an expired access token."""
Joe Gregorio695fdc12011-01-16 16:46:55 -050073 pass
74
Joe Gregoriof08a4982011-10-07 13:11:16 -040075class UnknownClientSecretsFlowError(Error):
76 """The client secrets file called for an unknown type of OAuth 2.0 flow. """
77 pass
78
Joe Gregorio695fdc12011-01-16 16:46:55 -050079
Joe Gregorio3b79fa82011-02-17 11:47:17 -050080class AccessTokenCredentialsError(Error):
81 """Having only the access_token means no refresh is possible."""
Joe Gregorio695fdc12011-01-16 16:46:55 -050082 pass
83
84
Joe Gregorio8b4c1732011-12-06 11:28:29 -050085class VerifyJwtTokenError(Error):
86 """Could on retrieve certificates for validation."""
87 pass
88
89
Joe Gregorio83f2ee62012-12-06 15:25:54 -050090class NonAsciiHeaderError(Error):
91 """Header names and values must be ASCII strings."""
92 pass
93
94
Joe Gregorio695fdc12011-01-16 16:46:55 -050095def _abstract():
96 raise NotImplementedError('You need to override this function')
97
98
Joe Gregorio9f2f38f2012-02-06 12:53:00 -050099class MemoryCache(object):
100 """httplib2 Cache implementation which only caches locally."""
101
102 def __init__(self):
103 self.cache = {}
104
105 def get(self, key):
106 return self.cache.get(key)
107
108 def set(self, key, value):
109 self.cache[key] = value
110
111 def delete(self, key):
112 self.cache.pop(key, None)
113
114
Joe Gregorio695fdc12011-01-16 16:46:55 -0500115class Credentials(object):
116 """Base class for all Credentials objects.
117
Joe Gregorio562b7312011-09-15 09:06:38 -0400118 Subclasses must define an authorize() method that applies the credentials to
119 an HTTP transport.
120
121 Subclasses must also specify a classmethod named 'from_json' that takes a JSON
Joe Gregoriofa8cd9f2012-02-23 14:00:40 -0500122 string as input and returns an instaniated Credentials object.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500123 """
124
Joe Gregorio562b7312011-09-15 09:06:38 -0400125 NON_SERIALIZED_MEMBERS = ['store']
126
Joe Gregorio695fdc12011-01-16 16:46:55 -0500127 def authorize(self, http):
128 """Take an httplib2.Http instance (or equivalent) and
129 authorizes it for the set of credentials, usually by
130 replacing http.request() with a method that adds in
131 the appropriate headers and then delegates to the original
132 Http.request() method.
133 """
134 _abstract()
135
Joe Gregorio654f4a22012-02-09 14:15:44 -0500136 def refresh(self, http):
137 """Forces a refresh of the access_token.
138
139 Args:
140 http: httplib2.Http, an http object to be used to make the refresh
141 request.
142 """
143 _abstract()
144
145 def apply(self, headers):
146 """Add the authorization to the headers.
147
148 Args:
149 headers: dict, the headers to add the Authorization header to.
150 """
151 _abstract()
152
Joe Gregorio562b7312011-09-15 09:06:38 -0400153 def _to_json(self, strip):
154 """Utility function for creating a JSON representation of an instance of Credentials.
155
156 Args:
157 strip: array, An array of names of members to not include in the JSON.
158
159 Returns:
160 string, a JSON representation of this instance, suitable to pass to
161 from_json().
162 """
163 t = type(self)
164 d = copy.copy(self.__dict__)
165 for member in strip:
Joe Gregorio08cdcb82012-03-14 00:09:33 -0400166 if member in d:
167 del d[member]
Joe Gregorio562b7312011-09-15 09:06:38 -0400168 if 'token_expiry' in d and isinstance(d['token_expiry'], datetime.datetime):
169 d['token_expiry'] = d['token_expiry'].strftime(EXPIRY_FORMAT)
170 # Add in information we will need later to reconsistitue this instance.
171 d['_class'] = t.__name__
172 d['_module'] = t.__module__
173 return simplejson.dumps(d)
174
175 def to_json(self):
176 """Creating a JSON representation of an instance of Credentials.
177
178 Returns:
179 string, a JSON representation of this instance, suitable to pass to
180 from_json().
181 """
182 return self._to_json(Credentials.NON_SERIALIZED_MEMBERS)
183
184 @classmethod
185 def new_from_json(cls, s):
186 """Utility class method to instantiate a Credentials subclass from a JSON
187 representation produced by to_json().
188
189 Args:
190 s: string, JSON from to_json().
191
192 Returns:
193 An instance of the subclass of Credentials that was serialized with
194 to_json().
195 """
196 data = simplejson.loads(s)
197 # Find and call the right classmethod from_json() to restore the object.
198 module = data['_module']
Joe Gregoriofa8cd9f2012-02-23 14:00:40 -0500199 try:
200 m = __import__(module)
201 except ImportError:
202 # In case there's an object from the old package structure, update it
203 module = module.replace('.apiclient', '')
204 m = __import__(module)
205
Joe Gregorioe9b40f12011-10-13 10:03:28 -0400206 m = __import__(module, fromlist=module.split('.')[:-1])
Joe Gregorio562b7312011-09-15 09:06:38 -0400207 kls = getattr(m, data['_class'])
208 from_json = getattr(kls, 'from_json')
209 return from_json(s)
210
Joe Gregorio08cdcb82012-03-14 00:09:33 -0400211 @classmethod
212 def from_json(cls, s):
Joe Gregorio401b8422012-05-03 16:35:35 -0400213 """Instantiate a Credentials object from a JSON description of it.
214
215 The JSON should have been produced by calling .to_json() on the object.
Joe Gregorio08cdcb82012-03-14 00:09:33 -0400216
217 Args:
218 data: dict, A deserialized JSON object.
219
220 Returns:
221 An instance of a Credentials subclass.
222 """
223 return Credentials()
224
JacobMoshenko8e905102011-06-20 09:53:10 -0400225
Joe Gregorio695fdc12011-01-16 16:46:55 -0500226class Flow(object):
227 """Base class for all Flow objects."""
228 pass
229
230
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500231class Storage(object):
232 """Base class for all Storage objects.
233
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400234 Store and retrieve a single credential. This class supports locking
235 such that multiple processes and threads can operate on a single
236 store.
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500237 """
238
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400239 def acquire_lock(self):
240 """Acquires any lock necessary to access this Storage.
241
Joe Gregorio401b8422012-05-03 16:35:35 -0400242 This lock is not reentrant.
243 """
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400244 pass
245
246 def release_lock(self):
247 """Release the Storage lock.
248
249 Trying to release a lock that isn't held will result in a
250 RuntimeError.
251 """
252 pass
253
254 def locked_get(self):
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500255 """Retrieve credential.
256
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400257 The Storage lock must be held when this is called.
258
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500259 Returns:
Joe Gregorio06d852b2011-03-25 15:03:10 -0400260 oauth2client.client.Credentials
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500261 """
262 _abstract()
263
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400264 def locked_put(self, credentials):
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500265 """Write a credential.
266
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400267 The Storage lock must be held when this is called.
268
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500269 Args:
270 credentials: Credentials, the credentials to store.
271 """
272 _abstract()
273
Joe Gregorioec75dc12012-02-06 13:40:42 -0500274 def locked_delete(self):
275 """Delete a credential.
276
277 The Storage lock must be held when this is called.
278 """
279 _abstract()
280
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400281 def get(self):
282 """Retrieve credential.
283
284 The Storage lock must *not* be held when this is called.
285
286 Returns:
287 oauth2client.client.Credentials
288 """
289 self.acquire_lock()
290 try:
291 return self.locked_get()
292 finally:
293 self.release_lock()
294
295 def put(self, credentials):
296 """Write a credential.
297
298 The Storage lock must be held when this is called.
299
300 Args:
301 credentials: Credentials, the credentials to store.
302 """
303 self.acquire_lock()
304 try:
305 self.locked_put(credentials)
306 finally:
307 self.release_lock()
308
Joe Gregorioec75dc12012-02-06 13:40:42 -0500309 def delete(self):
310 """Delete credential.
311
312 Frees any resources associated with storing the credential.
313 The Storage lock must *not* be held when this is called.
314
315 Returns:
316 None
317 """
318 self.acquire_lock()
319 try:
320 return self.locked_delete()
321 finally:
322 self.release_lock()
323
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500324
Joe Gregorio83f2ee62012-12-06 15:25:54 -0500325def clean_headers(headers):
326 """Forces header keys and values to be strings, i.e not unicode.
327
328 The httplib module just concats the header keys and values in a way that may
329 make the message header a unicode string, which, if it then tries to
330 contatenate to a binary request body may result in a unicode decode error.
331
332 Args:
333 headers: dict, A dictionary of headers.
334
335 Returns:
336 The same dictionary but with all the keys converted to strings.
337 """
338 clean = {}
339 try:
340 for k, v in headers.iteritems():
341 clean[str(k)] = str(v)
342 except UnicodeEncodeError:
343 raise NonAsciiHeaderError(k + ': ' + v)
344 return clean
345
346
Joe Gregorio695fdc12011-01-16 16:46:55 -0500347class OAuth2Credentials(Credentials):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400348 """Credentials object for OAuth 2.0.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500349
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500350 Credentials can be applied to an httplib2.Http object using the authorize()
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500351 method, which then adds the OAuth 2.0 access token to each request.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500352
353 OAuth2Credentials objects may be safely pickled and unpickled.
354 """
355
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400356 @util.positional(8)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500357 def __init__(self, access_token, client_id, client_secret, refresh_token,
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500358 token_expiry, token_uri, user_agent, id_token=None):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400359 """Create an instance of OAuth2Credentials.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500360
361 This constructor is not usually called by the user, instead
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500362 OAuth2Credentials objects are instantiated by the OAuth2WebServerFlow.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500363
364 Args:
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400365 access_token: string, access token.
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500366 client_id: string, client identifier.
367 client_secret: string, client secret.
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500368 refresh_token: string, refresh token.
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400369 token_expiry: datetime, when the access_token expires.
370 token_uri: string, URI of token endpoint.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500371 user_agent: string, The HTTP User-Agent to provide for this application.
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500372 id_token: object, The identity of the resource owner.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500373
Joe Gregorio695fdc12011-01-16 16:46:55 -0500374 Notes:
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500375 store: callable, A callable that when passed a Credential
Joe Gregorio695fdc12011-01-16 16:46:55 -0500376 will store the credential back to where it came from.
377 This is needed to store the latest access_token if it
378 has expired and been refreshed.
379 """
380 self.access_token = access_token
381 self.client_id = client_id
382 self.client_secret = client_secret
383 self.refresh_token = refresh_token
384 self.store = None
385 self.token_expiry = token_expiry
386 self.token_uri = token_uri
387 self.user_agent = user_agent
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500388 self.id_token = id_token
Joe Gregorio695fdc12011-01-16 16:46:55 -0500389
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500390 # True if the credentials have been revoked or expired and can't be
391 # refreshed.
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400392 self.invalid = False
Joe Gregorio9ce4b622011-02-17 15:32:11 -0500393
Joe Gregorio654f4a22012-02-09 14:15:44 -0500394 def authorize(self, http):
395 """Authorize an httplib2.Http instance with these credentials.
396
397 The modified http.request method will add authentication headers to each
398 request and will refresh access_tokens when a 401 is received on a
399 request. In addition the http.request method has a credentials property,
400 http.request.credentials, which is the Credentials object that authorized
401 it.
402
403 Args:
404 http: An instance of httplib2.Http
405 or something that acts like it.
406
407 Returns:
408 A modified instance of http that was passed in.
409
410 Example:
411
412 h = httplib2.Http()
413 h = credentials.authorize(h)
414
415 You can't create a new OAuth subclass of httplib2.Authenication
416 because it never gets passed the absolute URI, which is needed for
417 signing. So instead we have to overload 'request' with a closure
418 that adds in the Authorization header and then calls the original
419 version of 'request()'.
420 """
421 request_orig = http.request
422
423 # The closure that will replace 'httplib2.Http.request'.
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400424 @util.positional(1)
Joe Gregorio654f4a22012-02-09 14:15:44 -0500425 def new_request(uri, method='GET', body=None, headers=None,
426 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
427 connection_type=None):
428 if not self.access_token:
429 logger.info('Attempting refresh to obtain initial access_token')
430 self._refresh(request_orig)
431
432 # Modify the request headers to add the appropriate
433 # Authorization header.
434 if headers is None:
435 headers = {}
436 self.apply(headers)
437
438 if self.user_agent is not None:
439 if 'user-agent' in headers:
440 headers['user-agent'] = self.user_agent + ' ' + headers['user-agent']
441 else:
442 headers['user-agent'] = self.user_agent
443
Joe Gregorio83f2ee62012-12-06 15:25:54 -0500444 resp, content = request_orig(uri, method, body, clean_headers(headers),
Joe Gregorio654f4a22012-02-09 14:15:44 -0500445 redirections, connection_type)
446
Joe Gregorio7c7c6b12012-07-16 16:31:01 -0400447 # Older API (GData) respond with 403
448 if resp.status in [401, 403]:
449 logger.info('Refreshing due to a %s' % str(resp.status))
Joe Gregorio654f4a22012-02-09 14:15:44 -0500450 self._refresh(request_orig)
451 self.apply(headers)
Joe Gregorio83f2ee62012-12-06 15:25:54 -0500452 return request_orig(uri, method, body, clean_headers(headers),
Joe Gregorio654f4a22012-02-09 14:15:44 -0500453 redirections, connection_type)
454 else:
455 return (resp, content)
456
457 # Replace the request method with our own closure.
458 http.request = new_request
459
460 # Set credentials as a property of the request method.
461 setattr(http.request, 'credentials', self)
462
463 return http
464
465 def refresh(self, http):
466 """Forces a refresh of the access_token.
467
468 Args:
469 http: httplib2.Http, an http object to be used to make the refresh
470 request.
471 """
472 self._refresh(http.request)
473
474 def apply(self, headers):
475 """Add the authorization to the headers.
476
477 Args:
478 headers: dict, the headers to add the Authorization header to.
479 """
480 headers['Authorization'] = 'Bearer ' + self.access_token
481
Joe Gregorio562b7312011-09-15 09:06:38 -0400482 def to_json(self):
483 return self._to_json(Credentials.NON_SERIALIZED_MEMBERS)
484
485 @classmethod
486 def from_json(cls, s):
487 """Instantiate a Credentials object from a JSON description of it. The JSON
488 should have been produced by calling .to_json() on the object.
489
490 Args:
491 data: dict, A deserialized JSON object.
492
493 Returns:
494 An instance of a Credentials subclass.
495 """
496 data = simplejson.loads(s)
497 if 'token_expiry' in data and not isinstance(data['token_expiry'],
498 datetime.datetime):
Joe Gregorio1daa71b2011-09-15 18:12:14 -0400499 try:
500 data['token_expiry'] = datetime.datetime.strptime(
501 data['token_expiry'], EXPIRY_FORMAT)
502 except:
503 data['token_expiry'] = None
Joe Gregorio562b7312011-09-15 09:06:38 -0400504 retval = OAuth2Credentials(
505 data['access_token'],
506 data['client_id'],
507 data['client_secret'],
508 data['refresh_token'],
509 data['token_expiry'],
510 data['token_uri'],
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500511 data['user_agent'],
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400512 id_token=data.get('id_token', None))
Joe Gregorio562b7312011-09-15 09:06:38 -0400513 retval.invalid = data['invalid']
514 return retval
515
Joe Gregorio9ce4b622011-02-17 15:32:11 -0500516 @property
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400517 def access_token_expired(self):
518 """True if the credential is expired or invalid.
519
520 If the token_expiry isn't set, we assume the token doesn't expire.
521 """
522 if self.invalid:
523 return True
524
525 if not self.token_expiry:
526 return False
527
Joe Gregorio562b7312011-09-15 09:06:38 -0400528 now = datetime.datetime.utcnow()
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400529 if now >= self.token_expiry:
530 logger.info('access_token is expired. Now: %s, token_expiry: %s',
531 now, self.token_expiry)
532 return True
533 return False
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500534
Joe Gregorio695fdc12011-01-16 16:46:55 -0500535 def set_store(self, store):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400536 """Set the Storage for the credential.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500537
538 Args:
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400539 store: Storage, an implementation of Stroage object.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500540 This is needed to store the latest access_token if it
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400541 has expired and been refreshed. This implementation uses
542 locking to check for updates before updating the
543 access_token.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500544 """
545 self.store = store
546
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400547 def _updateFromCredential(self, other):
548 """Update this Credential from another instance."""
549 self.__dict__.update(other.__getstate__())
550
Joe Gregorio695fdc12011-01-16 16:46:55 -0500551 def __getstate__(self):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400552 """Trim the state down to something that can be pickled."""
Joe Gregorio695fdc12011-01-16 16:46:55 -0500553 d = copy.copy(self.__dict__)
554 del d['store']
555 return d
556
557 def __setstate__(self, state):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400558 """Reconstitute the state of the object from being pickled."""
Joe Gregorio695fdc12011-01-16 16:46:55 -0500559 self.__dict__.update(state)
560 self.store = None
561
JacobMoshenko8e905102011-06-20 09:53:10 -0400562 def _generate_refresh_request_body(self):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400563 """Generate the body that will be used in the refresh request."""
JacobMoshenko8e905102011-06-20 09:53:10 -0400564 body = urllib.urlencode({
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400565 'grant_type': 'refresh_token',
566 'client_id': self.client_id,
567 'client_secret': self.client_secret,
568 'refresh_token': self.refresh_token,
569 })
JacobMoshenko8e905102011-06-20 09:53:10 -0400570 return body
571
572 def _generate_refresh_request_headers(self):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400573 """Generate the headers that will be used in the refresh request."""
JacobMoshenko8e905102011-06-20 09:53:10 -0400574 headers = {
JacobMoshenko8e905102011-06-20 09:53:10 -0400575 'content-type': 'application/x-www-form-urlencoded',
576 }
JacobMoshenkocb6d8912011-07-08 13:35:15 -0400577
578 if self.user_agent is not None:
579 headers['user-agent'] = self.user_agent
580
JacobMoshenko8e905102011-06-20 09:53:10 -0400581 return headers
582
Joe Gregorio695fdc12011-01-16 16:46:55 -0500583 def _refresh(self, http_request):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400584 """Refreshes the access_token.
585
586 This method first checks by reading the Storage object if available.
587 If a refresh is still needed, it holds the Storage lock until the
588 refresh is completed.
Joe Gregorio654f4a22012-02-09 14:15:44 -0500589
590 Args:
591 http_request: callable, a callable that matches the method signature of
592 httplib2.Http.request, used to make the refresh request.
593
594 Raises:
595 AccessTokenRefreshError: When the refresh fails.
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400596 """
597 if not self.store:
598 self._do_refresh_request(http_request)
599 else:
600 self.store.acquire_lock()
601 try:
602 new_cred = self.store.locked_get()
603 if (new_cred and not new_cred.invalid and
604 new_cred.access_token != self.access_token):
605 logger.info('Updated access_token read from Storage')
606 self._updateFromCredential(new_cred)
607 else:
608 self._do_refresh_request(http_request)
609 finally:
610 self.store.release_lock()
611
612 def _do_refresh_request(self, http_request):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500613 """Refresh the access_token using the refresh_token.
614
615 Args:
Joe Gregorio654f4a22012-02-09 14:15:44 -0500616 http_request: callable, a callable that matches the method signature of
617 httplib2.Http.request, used to make the refresh request.
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400618
619 Raises:
620 AccessTokenRefreshError: When the refresh fails.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500621 """
JacobMoshenko8e905102011-06-20 09:53:10 -0400622 body = self._generate_refresh_request_body()
623 headers = self._generate_refresh_request_headers()
624
Joe Gregorio4b4002f2012-06-14 15:41:01 -0400625 logger.info('Refreshing access_token')
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500626 resp, content = http_request(
627 self.token_uri, method='POST', body=body, headers=headers)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500628 if resp.status == 200:
629 # TODO(jcgregorio) Raise an error if loads fails?
630 d = simplejson.loads(content)
631 self.access_token = d['access_token']
632 self.refresh_token = d.get('refresh_token', self.refresh_token)
633 if 'expires_in' in d:
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500634 self.token_expiry = datetime.timedelta(
Joe Gregorio562b7312011-09-15 09:06:38 -0400635 seconds=int(d['expires_in'])) + datetime.datetime.utcnow()
Joe Gregorio695fdc12011-01-16 16:46:55 -0500636 else:
637 self.token_expiry = None
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400638 if self.store:
639 self.store.locked_put(self)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500640 else:
JacobMoshenko8e905102011-06-20 09:53:10 -0400641 # An {'error':...} response body means the token is expired or revoked,
642 # so we flag the credentials as such.
Joe Gregorioe78621a2012-03-09 15:47:23 -0500643 logger.info('Failed to retrieve access token: %s' % content)
Joe Gregorioccc79542011-02-19 00:05:26 -0500644 error_msg = 'Invalid response %s.' % resp['status']
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500645 try:
646 d = simplejson.loads(content)
647 if 'error' in d:
Joe Gregorioccc79542011-02-19 00:05:26 -0500648 error_msg = d['error']
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400649 self.invalid = True
650 if self.store:
651 self.store.locked_put(self)
Joe Gregoriofd08e432012-08-09 14:17:41 -0400652 except StandardError:
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500653 pass
Joe Gregorioccc79542011-02-19 00:05:26 -0500654 raise AccessTokenRefreshError(error_msg)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500655
Joe Gregorio695fdc12011-01-16 16:46:55 -0500656
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500657class AccessTokenCredentials(OAuth2Credentials):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400658 """Credentials object for OAuth 2.0.
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500659
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400660 Credentials can be applied to an httplib2.Http object using the
661 authorize() method, which then signs each request from that object
662 with the OAuth 2.0 access token. This set of credentials is for the
663 use case where you have acquired an OAuth 2.0 access_token from
664 another place such as a JavaScript client or another web
665 application, and wish to use it from Python. Because only the
666 access_token is present it can not be refreshed and will in time
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500667 expire.
668
Joe Gregorio9ce4b622011-02-17 15:32:11 -0500669 AccessTokenCredentials objects may be safely pickled and unpickled.
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500670
671 Usage:
672 credentials = AccessTokenCredentials('<an access token>',
673 'my-user-agent/1.0')
674 http = httplib2.Http()
675 http = credentials.authorize(http)
676
677 Exceptions:
678 AccessTokenCredentialsExpired: raised when the access_token expires or is
679 revoked.
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500680 """
681
682 def __init__(self, access_token, user_agent):
683 """Create an instance of OAuth2Credentials
684
685 This is one of the few types if Credentials that you should contrust,
686 Credentials objects are usually instantiated by a Flow.
687
688 Args:
ade@google.com93a7f7c2011-02-23 16:00:37 +0000689 access_token: string, access token.
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500690 user_agent: string, The HTTP User-Agent to provide for this application.
691
692 Notes:
693 store: callable, a callable that when passed a Credential
694 will store the credential back to where it came from.
695 """
696 super(AccessTokenCredentials, self).__init__(
697 access_token,
698 None,
699 None,
700 None,
701 None,
702 None,
703 user_agent)
704
Joe Gregorio562b7312011-09-15 09:06:38 -0400705
706 @classmethod
707 def from_json(cls, s):
708 data = simplejson.loads(s)
709 retval = AccessTokenCredentials(
710 data['access_token'],
711 data['user_agent'])
712 return retval
713
Joe Gregorio3b79fa82011-02-17 11:47:17 -0500714 def _refresh(self, http_request):
715 raise AccessTokenCredentialsError(
716 "The access_token is expired or invalid and can't be refreshed.")
717
JacobMoshenko8e905102011-06-20 09:53:10 -0400718
719class AssertionCredentials(OAuth2Credentials):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400720 """Abstract Credentials object used for OAuth 2.0 assertion grants.
JacobMoshenko8e905102011-06-20 09:53:10 -0400721
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400722 This credential does not require a flow to instantiate because it
723 represents a two legged flow, and therefore has all of the required
724 information to generate and refresh its own access tokens. It must
725 be subclassed to generate the appropriate assertion string.
JacobMoshenko8e905102011-06-20 09:53:10 -0400726
727 AssertionCredentials objects may be safely pickled and unpickled.
728 """
729
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400730 @util.positional(2)
731 def __init__(self, assertion_type, user_agent=None,
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400732 token_uri='https://accounts.google.com/o/oauth2/token',
733 **unused_kwargs):
734 """Constructor for AssertionFlowCredentials.
JacobMoshenko8e905102011-06-20 09:53:10 -0400735
736 Args:
737 assertion_type: string, assertion type that will be declared to the auth
738 server
739 user_agent: string, The HTTP User-Agent to provide for this application.
740 token_uri: string, URI for token endpoint. For convenience
741 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
742 """
743 super(AssertionCredentials, self).__init__(
744 None,
745 None,
746 None,
747 None,
748 None,
749 token_uri,
750 user_agent)
751 self.assertion_type = assertion_type
752
753 def _generate_refresh_request_body(self):
754 assertion = self._generate_assertion()
755
756 body = urllib.urlencode({
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400757 'assertion_type': self.assertion_type,
758 'assertion': assertion,
759 'grant_type': 'assertion',
760 })
JacobMoshenko8e905102011-06-20 09:53:10 -0400761
762 return body
763
764 def _generate_assertion(self):
765 """Generate the assertion string that will be used in the access token
766 request.
767 """
768 _abstract()
769
Joe Gregorio0b723c22013-01-03 15:00:50 -0500770if HAS_CRYPTO:
771 # PyOpenSSL and PyCrypto are not prerequisites for oauth2client, so if it is
772 # missing then don't create the SignedJwtAssertionCredentials or the
773 # verify_id_token() method.
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500774
775 class SignedJwtAssertionCredentials(AssertionCredentials):
776 """Credentials object used for OAuth 2.0 Signed JWT assertion grants.
777
Joe Gregorio672051e2012-07-10 09:11:45 -0400778 This credential does not require a flow to instantiate because it represents
779 a two legged flow, and therefore has all of the required information to
780 generate and refresh its own access tokens.
781
Joe Gregorio0b723c22013-01-03 15:00:50 -0500782 SignedJwtAssertionCredentials requires either PyOpenSSL, or PyCrypto 2.6 or
783 later. For App Engine you may also consider using AppAssertionCredentials.
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500784 """
785
786 MAX_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds
787
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400788 @util.positional(4)
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500789 def __init__(self,
790 service_account_name,
791 private_key,
792 scope,
793 private_key_password='notasecret',
794 user_agent=None,
795 token_uri='https://accounts.google.com/o/oauth2/token',
796 **kwargs):
797 """Constructor for SignedJwtAssertionCredentials.
798
799 Args:
800 service_account_name: string, id for account, usually an email address.
Joe Gregorio0b723c22013-01-03 15:00:50 -0500801 private_key: string, private key in PKCS12 or PEM format.
Joe Gregorio5cf5d122012-11-16 16:36:12 -0500802 scope: string or iterable of strings, scope(s) of the credentials being
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500803 requested.
Joe Gregorio0b723c22013-01-03 15:00:50 -0500804 private_key_password: string, password for private_key, unused if
805 private_key is in PEM format.
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500806 user_agent: string, HTTP User-Agent to provide for this application.
807 token_uri: string, URI for token endpoint. For convenience
808 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
809 kwargs: kwargs, Additional parameters to add to the JWT token, for
810 example prn=joe@xample.org."""
811
812 super(SignedJwtAssertionCredentials, self).__init__(
813 'http://oauth.net/grant_type/jwt/1.0/bearer',
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400814 user_agent=user_agent,
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500815 token_uri=token_uri,
816 )
817
Joe Gregorio5cf5d122012-11-16 16:36:12 -0500818 self.scope = util.scopes_to_string(scope)
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500819
Joe Gregorioa19f3a72012-07-11 15:35:35 -0400820 # Keep base64 encoded so it can be stored in JSON.
821 self.private_key = base64.b64encode(private_key)
822
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500823 self.private_key_password = private_key_password
824 self.service_account_name = service_account_name
825 self.kwargs = kwargs
826
827 @classmethod
828 def from_json(cls, s):
829 data = simplejson.loads(s)
830 retval = SignedJwtAssertionCredentials(
831 data['service_account_name'],
Joe Gregorioa19f3a72012-07-11 15:35:35 -0400832 base64.b64decode(data['private_key']),
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500833 data['scope'],
Joe Gregorioa19f3a72012-07-11 15:35:35 -0400834 private_key_password=data['private_key_password'],
835 user_agent=data['user_agent'],
836 token_uri=data['token_uri'],
837 **data['kwargs']
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500838 )
839 retval.invalid = data['invalid']
Joe Gregorioa19f3a72012-07-11 15:35:35 -0400840 retval.access_token = data['access_token']
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500841 return retval
842
843 def _generate_assertion(self):
844 """Generate the assertion that will be used in the request."""
845 now = long(time.time())
846 payload = {
847 'aud': self.token_uri,
848 'scope': self.scope,
849 'iat': now,
850 'exp': now + SignedJwtAssertionCredentials.MAX_TOKEN_LIFETIME_SECS,
851 'iss': self.service_account_name
852 }
853 payload.update(self.kwargs)
Joe Gregorioe78621a2012-03-09 15:47:23 -0500854 logger.debug(str(payload))
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500855
Joe Gregorioa19f3a72012-07-11 15:35:35 -0400856 private_key = base64.b64decode(self.private_key)
Joe Gregorio0b723c22013-01-03 15:00:50 -0500857 return crypt.make_signed_jwt(crypt.Signer.from_string(
858 private_key, self.private_key_password), payload)
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500859
Joe Gregorio9f2f38f2012-02-06 12:53:00 -0500860 # Only used in verify_id_token(), which is always calling to the same URI
861 # for the certs.
862 _cached_http = httplib2.Http(MemoryCache())
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500863
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400864 @util.positional(2)
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500865 def verify_id_token(id_token, audience, http=None,
866 cert_uri=ID_TOKEN_VERIFICATON_CERTS):
867 """Verifies a signed JWT id_token.
868
Joe Gregorio672051e2012-07-10 09:11:45 -0400869 This function requires PyOpenSSL and because of that it does not work on
Joe Gregorio0b723c22013-01-03 15:00:50 -0500870 App Engine.
Joe Gregorio672051e2012-07-10 09:11:45 -0400871
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500872 Args:
873 id_token: string, A Signed JWT.
874 audience: string, The audience 'aud' that the token should be for.
875 http: httplib2.Http, instance to use to make the HTTP request. Callers
876 should supply an instance that has caching enabled.
877 cert_uri: string, URI of the certificates in JSON format to
878 verify the JWT against.
879
880 Returns:
881 The deserialized JSON in the JWT.
882
883 Raises:
884 oauth2client.crypt.AppIdentityError if the JWT fails to verify.
885 """
886 if http is None:
Joe Gregorio9f2f38f2012-02-06 12:53:00 -0500887 http = _cached_http
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500888
889 resp, content = http.request(cert_uri)
890
891 if resp.status == 200:
892 certs = simplejson.loads(content)
Joe Gregorio0b723c22013-01-03 15:00:50 -0500893 return crypt.verify_signed_jwt_with_certs(id_token, certs, audience)
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500894 else:
895 raise VerifyJwtTokenError('Status code: %d' % resp.status)
896
897
898def _urlsafe_b64decode(b64string):
Joe Gregoriobd512b52011-12-06 15:39:26 -0500899 # Guard against unicode strings, which base64 can't handle.
900 b64string = b64string.encode('ascii')
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500901 padded = b64string + '=' * (4 - len(b64string) % 4)
902 return base64.urlsafe_b64decode(padded)
903
904
905def _extract_id_token(id_token):
906 """Extract the JSON payload from a JWT.
907
908 Does the extraction w/o checking the signature.
909
910 Args:
911 id_token: string, OAuth 2.0 id_token.
912
913 Returns:
914 object, The deserialized JSON payload.
915 """
916 segments = id_token.split('.')
917
918 if (len(segments) != 3):
919 raise VerifyJwtTokenError(
920 'Wrong number of segments in token: %s' % id_token)
921
922 return simplejson.loads(_urlsafe_b64decode(segments[1]))
923
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400924
Joe Gregorioddb969a2012-07-11 11:04:12 -0400925def _parse_exchange_token_response(content):
926 """Parses response of an exchange token request.
927
928 Most providers return JSON but some (e.g. Facebook) return a
929 url-encoded string.
930
931 Args:
932 content: The body of a response
933
934 Returns:
935 Content as a dictionary object. Note that the dict could be empty,
936 i.e. {}. That basically indicates a failure.
937 """
938 resp = {}
939 try:
940 resp = simplejson.loads(content)
941 except StandardError:
942 # different JSON libs raise different exceptions,
943 # so we just do a catch-all here
944 resp = dict(parse_qsl(content))
945
946 # some providers respond with 'expires', others with 'expires_in'
947 if resp and 'expires' in resp:
948 resp['expires_in'] = resp.pop('expires')
949
950 return resp
951
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400952
953@util.positional(4)
Joe Gregorio32d852d2012-06-14 09:08:18 -0400954def credentials_from_code(client_id, client_secret, scope, code,
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400955 redirect_uri='postmessage', http=None, user_agent=None,
956 token_uri='https://accounts.google.com/o/oauth2/token'):
Joe Gregorio32d852d2012-06-14 09:08:18 -0400957 """Exchanges an authorization code for an OAuth2Credentials object.
958
959 Args:
960 client_id: string, client identifier.
961 client_secret: string, client secret.
Joe Gregorio5cf5d122012-11-16 16:36:12 -0500962 scope: string or iterable of strings, scope(s) to request.
Joe Gregorio32d852d2012-06-14 09:08:18 -0400963 code: string, An authroization code, most likely passed down from
964 the client
965 redirect_uri: string, this is generally set to 'postmessage' to match the
966 redirect_uri that the client specified
967 http: httplib2.Http, optional http instance to use to do the fetch
968 token_uri: string, URI for token endpoint. For convenience
969 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
970 Returns:
971 An OAuth2Credentials object.
972
973 Raises:
974 FlowExchangeError if the authorization code cannot be exchanged for an
975 access token
976 """
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400977 flow = OAuth2WebServerFlow(client_id, client_secret, scope,
978 redirect_uri=redirect_uri, user_agent=user_agent,
979 auth_uri='https://accounts.google.com/o/oauth2/auth',
980 token_uri=token_uri)
Joe Gregorio32d852d2012-06-14 09:08:18 -0400981
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400982 credentials = flow.step2_exchange(code, http=http)
Joe Gregorio32d852d2012-06-14 09:08:18 -0400983 return credentials
984
985
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400986@util.positional(3)
Joe Gregorio32d852d2012-06-14 09:08:18 -0400987def credentials_from_clientsecrets_and_code(filename, scope, code,
988 message = None,
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400989 redirect_uri='postmessage',
Joe Gregorioc29aaa92012-07-16 16:16:31 -0400990 http=None,
991 cache=None):
Joe Gregorio32d852d2012-06-14 09:08:18 -0400992 """Returns OAuth2Credentials from a clientsecrets file and an auth code.
993
994 Will create the right kind of Flow based on the contents of the clientsecrets
995 file or will raise InvalidClientSecretsError for unknown types of Flows.
996
997 Args:
998 filename: string, File name of clientsecrets.
Joe Gregorio5cf5d122012-11-16 16:36:12 -0500999 scope: string or iterable of strings, scope(s) to request.
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001000 code: string, An authorization code, most likely passed down from
Joe Gregorio32d852d2012-06-14 09:08:18 -04001001 the client
1002 message: string, A friendly string to display to the user if the
1003 clientsecrets file is missing or invalid. If message is provided then
1004 sys.exit will be called in the case of an error. If message in not
1005 provided then clientsecrets.InvalidClientSecretsError will be raised.
1006 redirect_uri: string, this is generally set to 'postmessage' to match the
1007 redirect_uri that the client specified
1008 http: httplib2.Http, optional http instance to use to do the fetch
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001009 cache: An optional cache service client that implements get() and set()
Joe Gregorioc29aaa92012-07-16 16:16:31 -04001010 methods. See clientsecrets.loadfile() for details.
Joe Gregorio32d852d2012-06-14 09:08:18 -04001011
1012 Returns:
1013 An OAuth2Credentials object.
1014
1015 Raises:
1016 FlowExchangeError if the authorization code cannot be exchanged for an
1017 access token
1018 UnknownClientSecretsFlowError if the file describes an unknown kind of Flow.
1019 clientsecrets.InvalidClientSecretsError if the clientsecrets file is
1020 invalid.
1021 """
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001022 flow = flow_from_clientsecrets(filename, scope, message=message, cache=cache,
1023 redirect_uri=redirect_uri)
1024 credentials = flow.step2_exchange(code, http=http)
Joe Gregorio32d852d2012-06-14 09:08:18 -04001025 return credentials
1026
JacobMoshenko8e905102011-06-20 09:53:10 -04001027
Joe Gregorio695fdc12011-01-16 16:46:55 -05001028class OAuth2WebServerFlow(Flow):
1029 """Does the Web Server Flow for OAuth 2.0.
1030
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001031 OAuth2WebServerFlow objects may be safely pickled and unpickled.
Joe Gregorio695fdc12011-01-16 16:46:55 -05001032 """
1033
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001034 @util.positional(4)
1035 def __init__(self, client_id, client_secret, scope,
1036 redirect_uri=None,
1037 user_agent=None,
Joe Gregorio9da2ad82011-09-11 14:04:44 -04001038 auth_uri='https://accounts.google.com/o/oauth2/auth',
1039 token_uri='https://accounts.google.com/o/oauth2/token',
1040 **kwargs):
1041 """Constructor for OAuth2WebServerFlow.
Joe Gregorio695fdc12011-01-16 16:46:55 -05001042
Joe Gregorio32f73192012-10-23 16:13:44 -04001043 The kwargs argument is used to set extra query parameters on the
1044 auth_uri. For example, the access_type and approval_prompt
1045 query parameters can be set via kwargs.
1046
Joe Gregorio695fdc12011-01-16 16:46:55 -05001047 Args:
Joe Gregorio7c22ab22011-02-16 15:32:39 -05001048 client_id: string, client identifier.
1049 client_secret: string client secret.
Joe Gregorio5cf5d122012-11-16 16:36:12 -05001050 scope: string or iterable of strings, scope(s) of the credentials being
Joe Gregoriof2f8a5a2011-10-14 15:11:29 -04001051 requested.
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001052 redirect_uri: string, Either the string 'urn:ietf:wg:oauth:2.0:oob' for
1053 a non-web-based application, or a URI that handles the callback from
1054 the authorization server.
Joe Gregorio695fdc12011-01-16 16:46:55 -05001055 user_agent: string, HTTP User-Agent to provide for this application.
Joe Gregorio7c22ab22011-02-16 15:32:39 -05001056 auth_uri: string, URI for authorization endpoint. For convenience
1057 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
1058 token_uri: string, URI for token endpoint. For convenience
1059 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
Joe Gregorio695fdc12011-01-16 16:46:55 -05001060 **kwargs: dict, The keyword arguments are all optional and required
1061 parameters for the OAuth calls.
1062 """
1063 self.client_id = client_id
1064 self.client_secret = client_secret
Joe Gregorio5cf5d122012-11-16 16:36:12 -05001065 self.scope = util.scopes_to_string(scope)
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001066 self.redirect_uri = redirect_uri
Joe Gregorio695fdc12011-01-16 16:46:55 -05001067 self.user_agent = user_agent
Joe Gregorio7c22ab22011-02-16 15:32:39 -05001068 self.auth_uri = auth_uri
Joe Gregorio695fdc12011-01-16 16:46:55 -05001069 self.token_uri = token_uri
Joe Gregorio69a0aca2011-11-03 10:47:32 -04001070 self.params = {
1071 'access_type': 'offline',
Joe Gregorio32f73192012-10-23 16:13:44 -04001072 'response_type': 'code',
Joe Gregorio69a0aca2011-11-03 10:47:32 -04001073 }
1074 self.params.update(kwargs)
Joe Gregorio695fdc12011-01-16 16:46:55 -05001075
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001076 @util.positional(1)
1077 def step1_get_authorize_url(self, redirect_uri=None):
Joe Gregorio695fdc12011-01-16 16:46:55 -05001078 """Returns a URI to redirect to the provider.
1079
1080 Args:
Joe Gregoriof2326c02012-02-09 12:18:44 -05001081 redirect_uri: string, Either the string 'urn:ietf:wg:oauth:2.0:oob' for
1082 a non-web-based application, or a URI that handles the callback from
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001083 the authorization server. This parameter is deprecated, please move to
1084 passing the redirect_uri in via the constructor.
Joe Gregorio695fdc12011-01-16 16:46:55 -05001085
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001086 Returns:
1087 A URI as a string to redirect the user to begin the authorization flow.
Joe Gregorio695fdc12011-01-16 16:46:55 -05001088 """
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001089 if redirect_uri is not None:
1090 logger.warning(('The redirect_uri parameter for'
1091 'OAuth2WebServerFlow.step1_get_authorize_url is deprecated. Please'
1092 'move to passing the redirect_uri in via the constructor.'))
1093 self.redirect_uri = redirect_uri
Joe Gregorio695fdc12011-01-16 16:46:55 -05001094
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001095 if self.redirect_uri is None:
1096 raise ValueError('The value of redirect_uri must not be None.')
1097
Joe Gregorio695fdc12011-01-16 16:46:55 -05001098 query = {
Joe Gregorio9da2ad82011-09-11 14:04:44 -04001099 'client_id': self.client_id,
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001100 'redirect_uri': self.redirect_uri,
Joe Gregorio9da2ad82011-09-11 14:04:44 -04001101 'scope': self.scope,
1102 }
Joe Gregorio695fdc12011-01-16 16:46:55 -05001103 query.update(self.params)
Joe Gregorio7c22ab22011-02-16 15:32:39 -05001104 parts = list(urlparse.urlparse(self.auth_uri))
Joe Gregorio695fdc12011-01-16 16:46:55 -05001105 query.update(dict(parse_qsl(parts[4]))) # 4 is the index of the query part
1106 parts[4] = urllib.urlencode(query)
1107 return urlparse.urlunparse(parts)
1108
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001109 @util.positional(2)
Joe Gregorioccc79542011-02-19 00:05:26 -05001110 def step2_exchange(self, code, http=None):
Joe Gregorio695fdc12011-01-16 16:46:55 -05001111 """Exhanges a code for OAuth2Credentials.
1112
1113 Args:
1114 code: string or dict, either the code as a string, or a dictionary
1115 of the query parameters to the redirect_uri, which contains
1116 the code.
Joe Gregorioccc79542011-02-19 00:05:26 -05001117 http: httplib2.Http, optional http instance to use to do the fetch
Joe Gregorio4b4002f2012-06-14 15:41:01 -04001118
1119 Returns:
1120 An OAuth2Credentials object that can be used to authorize requests.
1121
1122 Raises:
1123 FlowExchangeError if a problem occured exchanging the code for a
1124 refresh_token.
Joe Gregorio695fdc12011-01-16 16:46:55 -05001125 """
1126
1127 if not (isinstance(code, str) or isinstance(code, unicode)):
Joe Gregorio4b4002f2012-06-14 15:41:01 -04001128 if 'code' not in code:
1129 if 'error' in code:
1130 error_msg = code['error']
1131 else:
1132 error_msg = 'No code was supplied in the query parameters.'
1133 raise FlowExchangeError(error_msg)
1134 else:
1135 code = code['code']
Joe Gregorio695fdc12011-01-16 16:46:55 -05001136
1137 body = urllib.urlencode({
Joe Gregorio9da2ad82011-09-11 14:04:44 -04001138 'grant_type': 'authorization_code',
1139 'client_id': self.client_id,
1140 'client_secret': self.client_secret,
1141 'code': code,
1142 'redirect_uri': self.redirect_uri,
1143 'scope': self.scope,
1144 })
Joe Gregorio695fdc12011-01-16 16:46:55 -05001145 headers = {
Joe Gregorio9da2ad82011-09-11 14:04:44 -04001146 'content-type': 'application/x-www-form-urlencoded',
Joe Gregorio695fdc12011-01-16 16:46:55 -05001147 }
JacobMoshenkocb6d8912011-07-08 13:35:15 -04001148
1149 if self.user_agent is not None:
1150 headers['user-agent'] = self.user_agent
1151
Joe Gregorioccc79542011-02-19 00:05:26 -05001152 if http is None:
1153 http = httplib2.Http()
Joe Gregorio8b4c1732011-12-06 11:28:29 -05001154
JacobMoshenko8e905102011-06-20 09:53:10 -04001155 resp, content = http.request(self.token_uri, method='POST', body=body,
1156 headers=headers)
Joe Gregorioddb969a2012-07-11 11:04:12 -04001157 d = _parse_exchange_token_response(content)
1158 if resp.status == 200 and 'access_token' in d:
Joe Gregorio695fdc12011-01-16 16:46:55 -05001159 access_token = d['access_token']
1160 refresh_token = d.get('refresh_token', None)
1161 token_expiry = None
1162 if 'expires_in' in d:
Joe Gregorio562b7312011-09-15 09:06:38 -04001163 token_expiry = datetime.datetime.utcnow() + datetime.timedelta(
JacobMoshenko8e905102011-06-20 09:53:10 -04001164 seconds=int(d['expires_in']))
Joe Gregorio695fdc12011-01-16 16:46:55 -05001165
Joe Gregorio8b4c1732011-12-06 11:28:29 -05001166 if 'id_token' in d:
1167 d['id_token'] = _extract_id_token(d['id_token'])
1168
Joe Gregorio6ceea2d2012-08-24 11:57:58 -04001169 logger.info('Successfully retrieved access token')
JacobMoshenko8e905102011-06-20 09:53:10 -04001170 return OAuth2Credentials(access_token, self.client_id,
1171 self.client_secret, refresh_token, token_expiry,
Joe Gregorio8b4c1732011-12-06 11:28:29 -05001172 self.token_uri, self.user_agent,
1173 id_token=d.get('id_token', None))
Joe Gregorio695fdc12011-01-16 16:46:55 -05001174 else:
Joe Gregorioe78621a2012-03-09 15:47:23 -05001175 logger.info('Failed to retrieve access token: %s' % content)
Joe Gregorioddb969a2012-07-11 11:04:12 -04001176 if 'error' in d:
1177 # you never know what those providers got to say
1178 error_msg = unicode(d['error'])
1179 else:
1180 error_msg = 'Invalid response: %s.' % str(resp.status)
Joe Gregorioccc79542011-02-19 00:05:26 -05001181 raise FlowExchangeError(error_msg)
Joe Gregoriof08a4982011-10-07 13:11:16 -04001182
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001183
1184@util.positional(2)
1185def flow_from_clientsecrets(filename, scope, redirect_uri=None, message=None, cache=None):
Joe Gregoriof08a4982011-10-07 13:11:16 -04001186 """Create a Flow from a clientsecrets file.
1187
1188 Will create the right kind of Flow based on the contents of the clientsecrets
1189 file or will raise InvalidClientSecretsError for unknown types of Flows.
1190
1191 Args:
1192 filename: string, File name of client secrets.
Joe Gregorio5cf5d122012-11-16 16:36:12 -05001193 scope: string or iterable of strings, scope(s) to request.
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001194 redirect_uri: string, Either the string 'urn:ietf:wg:oauth:2.0:oob' for
1195 a non-web-based application, or a URI that handles the callback from
1196 the authorization server.
Joe Gregoriof08a4982011-10-07 13:11:16 -04001197 message: string, A friendly string to display to the user if the
1198 clientsecrets file is missing or invalid. If message is provided then
1199 sys.exit will be called in the case of an error. If message in not
1200 provided then clientsecrets.InvalidClientSecretsError will be raised.
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001201 cache: An optional cache service client that implements get() and set()
Joe Gregorioc29aaa92012-07-16 16:16:31 -04001202 methods. See clientsecrets.loadfile() for details.
Joe Gregoriof08a4982011-10-07 13:11:16 -04001203
1204 Returns:
1205 A Flow object.
1206
1207 Raises:
1208 UnknownClientSecretsFlowError if the file describes an unknown kind of Flow.
1209 clientsecrets.InvalidClientSecretsError if the clientsecrets file is
1210 invalid.
1211 """
Joe Gregorio0984ef22011-10-14 13:17:43 -04001212 try:
Joe Gregorioc29aaa92012-07-16 16:16:31 -04001213 client_type, client_info = clientsecrets.loadfile(filename, cache=cache)
Joe Gregorio0984ef22011-10-14 13:17:43 -04001214 if client_type in [clientsecrets.TYPE_WEB, clientsecrets.TYPE_INSTALLED]:
1215 return OAuth2WebServerFlow(
1216 client_info['client_id'],
1217 client_info['client_secret'],
1218 scope,
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001219 redirect_uri=redirect_uri,
1220 user_agent=None,
1221 auth_uri=client_info['auth_uri'],
1222 token_uri=client_info['token_uri'])
1223
Joe Gregorio0984ef22011-10-14 13:17:43 -04001224 except clientsecrets.InvalidClientSecretsError:
1225 if message:
1226 sys.exit(message)
1227 else:
1228 raise
Joe Gregoriof08a4982011-10-07 13:11:16 -04001229 else:
1230 raise UnknownClientSecretsFlowError(
1231 'This OAuth 2.0 flow is unsupported: "%s"' * client_type)