Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(242)

Side by Side Diff: client/third_party/oauth2client/contrib/appengine.py

Issue 1768993002: Update oauth2client to v2.0.1 and googleapiclient to v1.5.0. Base URL: git@github.com:luci/luci-py.git@master
Patch Set: . Created 4 years, 9 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
1 # Copyright 2014 Google Inc. All rights reserved.
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.
14
15 """Utilities for Google App Engine
16
17 Utilities for making it easier to use OAuth 2.0 on Google App Engine.
18 """
19
20 import cgi
21 import json
22 import logging
23 import os
24 import pickle
25 import threading
26
27 import httplib2
28 import webapp2 as webapp
29
30 from google.appengine.api import app_identity
31 from google.appengine.api import memcache
32 from google.appengine.api import users
33 from google.appengine.ext import db
34 from google.appengine.ext.webapp.util import login_required
35
36 from oauth2client import GOOGLE_AUTH_URI
37 from oauth2client import GOOGLE_REVOKE_URI
38 from oauth2client import GOOGLE_TOKEN_URI
39 from oauth2client import clientsecrets
40 from oauth2client import util
41 from oauth2client.client import AccessTokenRefreshError
42 from oauth2client.client import AssertionCredentials
43 from oauth2client.client import Credentials
44 from oauth2client.client import Flow
45 from oauth2client.client import OAuth2WebServerFlow
46 from oauth2client.client import Storage
47 from oauth2client.contrib import xsrfutil
48
49 # This is a temporary fix for a Google internal issue.
50 try:
51 from oauth2client.contrib import _appengine_ndb
52 except ImportError: # pragma: NO COVER
53 _appengine_ndb = None
54
55
56 __author__ = 'jcgregorio@google.com (Joe Gregorio)'
57
58 logger = logging.getLogger(__name__)
59
60 OAUTH2CLIENT_NAMESPACE = 'oauth2client#ns'
61
62 XSRF_MEMCACHE_ID = 'xsrf_secret_key'
63
64 if _appengine_ndb is None:
65 CredentialsNDBModel = None
66 CredentialsNDBProperty = None
67 FlowNDBProperty = None
68 _NDB_KEY = None
69 _NDB_MODEL = None
70 SiteXsrfSecretKeyNDB = None
71 else:
72 CredentialsNDBModel = _appengine_ndb.CredentialsNDBModel
73 CredentialsNDBProperty = _appengine_ndb.CredentialsNDBProperty
74 FlowNDBProperty = _appengine_ndb.FlowNDBProperty
75 _NDB_KEY = _appengine_ndb.NDB_KEY
76 _NDB_MODEL = _appengine_ndb.NDB_MODEL
77 SiteXsrfSecretKeyNDB = _appengine_ndb.SiteXsrfSecretKeyNDB
78
79
80 def _safe_html(s):
81 """Escape text to make it safe to display.
82
83 Args:
84 s: string, The text to escape.
85
86 Returns:
87 The escaped text as a string.
88 """
89 return cgi.escape(s, quote=1).replace("'", ''')
90
91
92 class InvalidClientSecretsError(Exception):
93 """The client_secrets.json file is malformed or missing required fields."""
94
95
96 class InvalidXsrfTokenError(Exception):
97 """The XSRF token is invalid or expired."""
98
99
100 class SiteXsrfSecretKey(db.Model):
101 """Storage for the sites XSRF secret key.
102
103 There will only be one instance stored of this model, the one used for the
104 site.
105 """
106 secret = db.StringProperty()
107
108
109 def _generate_new_xsrf_secret_key():
110 """Returns a random XSRF secret key."""
111 return os.urandom(16).encode("hex")
112
113
114 def xsrf_secret_key():
115 """Return the secret key for use for XSRF protection.
116
117 If the Site entity does not have a secret key, this method will also create
118 one and persist it.
119
120 Returns:
121 The secret key.
122 """
123 secret = memcache.get(XSRF_MEMCACHE_ID, namespace=OAUTH2CLIENT_NAMESPACE)
124 if not secret:
125 # Load the one and only instance of SiteXsrfSecretKey.
126 model = SiteXsrfSecretKey.get_or_insert(key_name='site')
127 if not model.secret:
128 model.secret = _generate_new_xsrf_secret_key()
129 model.put()
130 secret = model.secret
131 memcache.add(XSRF_MEMCACHE_ID, secret,
132 namespace=OAUTH2CLIENT_NAMESPACE)
133
134 return str(secret)
135
136
137 class AppAssertionCredentials(AssertionCredentials):
138 """Credentials object for App Engine Assertion Grants
139
140 This object will allow an App Engine application to identify itself to
141 Google and other OAuth 2.0 servers that can verify assertions. It can be
142 used for the purpose of accessing data stored under an account assigned to
143 the App Engine application itself.
144
145 This credential does not require a flow to instantiate because it
146 represents a two legged flow, and therefore has all of the required
147 information to generate and refresh its own access tokens.
148 """
149
150 @util.positional(2)
151 def __init__(self, scope, **kwargs):
152 """Constructor for AppAssertionCredentials
153
154 Args:
155 scope: string or iterable of strings, scope(s) of the credentials
156 being requested.
157 **kwargs: optional keyword args, including:
158 service_account_id: service account id of the application. If None
159 or unspecified, the default service account for
160 the app is used.
161 """
162 self.scope = util.scopes_to_string(scope)
163 self._kwargs = kwargs
164 self.service_account_id = kwargs.get('service_account_id', None)
165 self._service_account_email = None
166
167 # Assertion type is no longer used, but still in the
168 # parent class signature.
169 super(AppAssertionCredentials, self).__init__(None)
170
171 @classmethod
172 def from_json(cls, json_data):
173 data = json.loads(json_data)
174 return AppAssertionCredentials(data['scope'])
175
176 def _refresh(self, http_request):
177 """Refreshes the access_token.
178
179 Since the underlying App Engine app_identity implementation does its
180 own caching we can skip all the storage hoops and just to a refresh
181 using the API.
182
183 Args:
184 http_request: callable, a callable that matches the method
185 signature of httplib2.Http.request, used to make the
186 refresh request.
187
188 Raises:
189 AccessTokenRefreshError: When the refresh fails.
190 """
191 try:
192 scopes = self.scope.split()
193 (token, _) = app_identity.get_access_token(
194 scopes, service_account_id=self.service_account_id)
195 except app_identity.Error as e:
196 raise AccessTokenRefreshError(str(e))
197 self.access_token = token
198
199 @property
200 def serialization_data(self):
201 raise NotImplementedError('Cannot serialize credentials '
202 'for Google App Engine.')
203
204 def create_scoped_required(self):
205 return not self.scope
206
207 def create_scoped(self, scopes):
208 return AppAssertionCredentials(scopes, **self._kwargs)
209
210 def sign_blob(self, blob):
211 """Cryptographically sign a blob (of bytes).
212
213 Implements abstract method
214 :meth:`oauth2client.client.AssertionCredentials.sign_blob`.
215
216 Args:
217 blob: bytes, Message to be signed.
218
219 Returns:
220 tuple, A pair of the private key ID used to sign the blob and
221 the signed contents.
222 """
223 return app_identity.sign_blob(blob)
224
225 @property
226 def service_account_email(self):
227 """Get the email for the current service account.
228
229 Returns:
230 string, The email associated with the Google App Engine
231 service account.
232 """
233 if self._service_account_email is None:
234 self._service_account_email = (
235 app_identity.get_service_account_name())
236 return self._service_account_email
237
238
239 class FlowProperty(db.Property):
240 """App Engine datastore Property for Flow.
241
242 Utility property that allows easy storage and retrieval of an
243 oauth2client.Flow
244 """
245
246 # Tell what the user type is.
247 data_type = Flow
248
249 # For writing to datastore.
250 def get_value_for_datastore(self, model_instance):
251 flow = super(FlowProperty, self).get_value_for_datastore(
252 model_instance)
253 return db.Blob(pickle.dumps(flow))
254
255 # For reading from datastore.
256 def make_value_from_datastore(self, value):
257 if value is None:
258 return None
259 return pickle.loads(value)
260
261 def validate(self, value):
262 if value is not None and not isinstance(value, Flow):
263 raise db.BadValueError('Property %s must be convertible '
264 'to a FlowThreeLegged instance (%s)' %
265 (self.name, value))
266 return super(FlowProperty, self).validate(value)
267
268 def empty(self, value):
269 return not value
270
271
272 class CredentialsProperty(db.Property):
273 """App Engine datastore Property for Credentials.
274
275 Utility property that allows easy storage and retrieval of
276 oath2client.Credentials
277 """
278
279 # Tell what the user type is.
280 data_type = Credentials
281
282 # For writing to datastore.
283 def get_value_for_datastore(self, model_instance):
284 logger.info("get: Got type " + str(type(model_instance)))
285 cred = super(CredentialsProperty, self).get_value_for_datastore(
286 model_instance)
287 if cred is None:
288 cred = ''
289 else:
290 cred = cred.to_json()
291 return db.Blob(cred)
292
293 # For reading from datastore.
294 def make_value_from_datastore(self, value):
295 logger.info("make: Got type " + str(type(value)))
296 if value is None:
297 return None
298 if len(value) == 0:
299 return None
300 try:
301 credentials = Credentials.new_from_json(value)
302 except ValueError:
303 credentials = None
304 return credentials
305
306 def validate(self, value):
307 value = super(CredentialsProperty, self).validate(value)
308 logger.info("validate: Got type " + str(type(value)))
309 if value is not None and not isinstance(value, Credentials):
310 raise db.BadValueError('Property %s must be convertible '
311 'to a Credentials instance (%s)' %
312 (self.name, value))
313 return value
314
315
316 class StorageByKeyName(Storage):
317 """Store and retrieve a credential to and from the App Engine datastore.
318
319 This Storage helper presumes the Credentials have been stored as a
320 CredentialsProperty or CredentialsNDBProperty on a datastore model class,
321 and that entities are stored by key_name.
322 """
323
324 @util.positional(4)
325 def __init__(self, model, key_name, property_name, cache=None, user=None):
326 """Constructor for Storage.
327
328 Args:
329 model: db.Model or ndb.Model, model class
330 key_name: string, key name for the entity that has the credentials
331 property_name: string, name of the property that is a
332 CredentialsProperty or CredentialsNDBProperty.
333 cache: memcache, a write-through cache to put in front of the
334 datastore. If the model you are using is an NDB model, using
335 a cache will be redundant since the model uses an instance
336 cache and memcache for you.
337 user: users.User object, optional. Can be used to grab user ID as a
338 key_name if no key name is specified.
339 """
340 super(StorageByKeyName, self).__init__()
341
342 if key_name is None:
343 if user is None:
344 raise ValueError('StorageByKeyName called with no '
345 'key name or user.')
346 key_name = user.user_id()
347
348 self._model = model
349 self._key_name = key_name
350 self._property_name = property_name
351 self._cache = cache
352
353 def _is_ndb(self):
354 """Determine whether the model of the instance is an NDB model.
355
356 Returns:
357 Boolean indicating whether or not the model is an NDB or DB model.
358 """
359 # issubclass will fail if one of the arguments is not a class, only
360 # need worry about new-style classes since ndb and db models are
361 # new-style
362 if isinstance(self._model, type):
363 if _NDB_MODEL is not None and issubclass(self._model, _NDB_MODEL):
364 return True
365 elif issubclass(self._model, db.Model):
366 return False
367
368 raise TypeError('Model class not an NDB or DB model: %s.' %
369 (self._model,))
370
371 def _get_entity(self):
372 """Retrieve entity from datastore.
373
374 Uses a different model method for db or ndb models.
375
376 Returns:
377 Instance of the model corresponding to the current storage object
378 and stored using the key name of the storage object.
379 """
380 if self._is_ndb():
381 return self._model.get_by_id(self._key_name)
382 else:
383 return self._model.get_by_key_name(self._key_name)
384
385 def _delete_entity(self):
386 """Delete entity from datastore.
387
388 Attempts to delete using the key_name stored on the object, whether or
389 not the given key is in the datastore.
390 """
391 if self._is_ndb():
392 _NDB_KEY(self._model, self._key_name).delete()
393 else:
394 entity_key = db.Key.from_path(self._model.kind(), self._key_name)
395 db.delete(entity_key)
396
397 @db.non_transactional(allow_existing=True)
398 def locked_get(self):
399 """Retrieve Credential from datastore.
400
401 Returns:
402 oauth2client.Credentials
403 """
404 credentials = None
405 if self._cache:
406 json = self._cache.get(self._key_name)
407 if json:
408 credentials = Credentials.new_from_json(json)
409 if credentials is None:
410 entity = self._get_entity()
411 if entity is not None:
412 credentials = getattr(entity, self._property_name)
413 if self._cache:
414 self._cache.set(self._key_name, credentials.to_json())
415
416 if credentials and hasattr(credentials, 'set_store'):
417 credentials.set_store(self)
418 return credentials
419
420 @db.non_transactional(allow_existing=True)
421 def locked_put(self, credentials):
422 """Write a Credentials to the datastore.
423
424 Args:
425 credentials: Credentials, the credentials to store.
426 """
427 entity = self._model.get_or_insert(self._key_name)
428 setattr(entity, self._property_name, credentials)
429 entity.put()
430 if self._cache:
431 self._cache.set(self._key_name, credentials.to_json())
432
433 @db.non_transactional(allow_existing=True)
434 def locked_delete(self):
435 """Delete Credential from datastore."""
436
437 if self._cache:
438 self._cache.delete(self._key_name)
439
440 self._delete_entity()
441
442
443 class CredentialsModel(db.Model):
444 """Storage for OAuth 2.0 Credentials
445
446 Storage of the model is keyed by the user.user_id().
447 """
448 credentials = CredentialsProperty()
449
450
451 def _build_state_value(request_handler, user):
452 """Composes the value for the 'state' parameter.
453
454 Packs the current request URI and an XSRF token into an opaque string that
455 can be passed to the authentication server via the 'state' parameter.
456
457 Args:
458 request_handler: webapp.RequestHandler, The request.
459 user: google.appengine.api.users.User, The current user.
460
461 Returns:
462 The state value as a string.
463 """
464 uri = request_handler.request.url
465 token = xsrfutil.generate_token(xsrf_secret_key(), user.user_id(),
466 action_id=str(uri))
467 return uri + ':' + token
468
469
470 def _parse_state_value(state, user):
471 """Parse the value of the 'state' parameter.
472
473 Parses the value and validates the XSRF token in the state parameter.
474
475 Args:
476 state: string, The value of the state parameter.
477 user: google.appengine.api.users.User, The current user.
478
479 Raises:
480 InvalidXsrfTokenError: if the XSRF token is invalid.
481
482 Returns:
483 The redirect URI.
484 """
485 uri, token = state.rsplit(':', 1)
486 if not xsrfutil.validate_token(xsrf_secret_key(), token, user.user_id(),
487 action_id=uri):
488 raise InvalidXsrfTokenError()
489
490 return uri
491
492
493 class OAuth2Decorator(object):
494 """Utility for making OAuth 2.0 easier.
495
496 Instantiate and then use with oauth_required or oauth_aware
497 as decorators on webapp.RequestHandler methods.
498
499 ::
500
501 decorator = OAuth2Decorator(
502 client_id='837...ent.com',
503 client_secret='Qh...wwI',
504 scope='https://www.googleapis.com/auth/plus')
505
506 class MainHandler(webapp.RequestHandler):
507 @decorator.oauth_required
508 def get(self):
509 http = decorator.http()
510 # http is authorized with the user's Credentials and can be
511 # used in API calls
512
513 """
514
515 def set_credentials(self, credentials):
516 self._tls.credentials = credentials
517
518 def get_credentials(self):
519 """A thread local Credentials object.
520
521 Returns:
522 A client.Credentials object, or None if credentials hasn't been set
523 in this thread yet, which may happen when calling has_credentials
524 inside oauth_aware.
525 """
526 return getattr(self._tls, 'credentials', None)
527
528 credentials = property(get_credentials, set_credentials)
529
530 def set_flow(self, flow):
531 self._tls.flow = flow
532
533 def get_flow(self):
534 """A thread local Flow object.
535
536 Returns:
537 A credentials.Flow object, or None if the flow hasn't been set in
538 this thread yet, which happens in _create_flow() since Flows are
539 created lazily.
540 """
541 return getattr(self._tls, 'flow', None)
542
543 flow = property(get_flow, set_flow)
544
545 @util.positional(4)
546 def __init__(self, client_id, client_secret, scope,
547 auth_uri=GOOGLE_AUTH_URI,
548 token_uri=GOOGLE_TOKEN_URI,
549 revoke_uri=GOOGLE_REVOKE_URI,
550 user_agent=None,
551 message=None,
552 callback_path='/oauth2callback',
553 token_response_param=None,
554 _storage_class=StorageByKeyName,
555 _credentials_class=CredentialsModel,
556 _credentials_property_name='credentials',
557 **kwargs):
558 """Constructor for OAuth2Decorator
559
560 Args:
561 client_id: string, client identifier.
562 client_secret: string client secret.
563 scope: string or iterable of strings, scope(s) of the credentials
564 being requested.
565 auth_uri: string, URI for authorization endpoint. For convenience
566 defaults to Google's endpoints but any OAuth 2.0 provider
567 can be used.
568 token_uri: string, URI for token endpoint. For convenience defaults
569 to Google's endpoints but any OAuth 2.0 provider can be
570 used.
571 revoke_uri: string, URI for revoke endpoint. For convenience
572 defaults to Google's endpoints but any OAuth 2.0
573 provider can be used.
574 user_agent: string, User agent of your application, default to
575 None.
576 message: Message to display if there are problems with the
577 OAuth 2.0 configuration. The message may contain HTML and
578 will be presented on the web interface for any method that
579 uses the decorator.
580 callback_path: string, The absolute path to use as the callback
581 URI. Note that this must match up with the URI given
582 when registering the application in the APIs
583 Console.
584 token_response_param: string. If provided, the full JSON response
585 to the access token request will be encoded
586 and included in this query parameter in the
587 callback URI. This is useful with providers
588 (e.g. wordpress.com) that include extra
589 fields that the client may want.
590 _storage_class: "Protected" keyword argument not typically provided
591 to this constructor. A storage class to aid in
592 storing a Credentials object for a user in the
593 datastore. Defaults to StorageByKeyName.
594 _credentials_class: "Protected" keyword argument not typically
595 provided to this constructor. A db or ndb Model
596 class to hold credentials. Defaults to
597 CredentialsModel.
598 _credentials_property_name: "Protected" keyword argument not
599 typically provided to this constructor.
600 A string indicating the name of the
601 field on the _credentials_class where a
602 Credentials object will be stored.
603 Defaults to 'credentials'.
604 **kwargs: dict, Keyword arguments are passed along as kwargs to
605 the OAuth2WebServerFlow constructor.
606 """
607 self._tls = threading.local()
608 self.flow = None
609 self.credentials = None
610 self._client_id = client_id
611 self._client_secret = client_secret
612 self._scope = util.scopes_to_string(scope)
613 self._auth_uri = auth_uri
614 self._token_uri = token_uri
615 self._revoke_uri = revoke_uri
616 self._user_agent = user_agent
617 self._kwargs = kwargs
618 self._message = message
619 self._in_error = False
620 self._callback_path = callback_path
621 self._token_response_param = token_response_param
622 self._storage_class = _storage_class
623 self._credentials_class = _credentials_class
624 self._credentials_property_name = _credentials_property_name
625
626 def _display_error_message(self, request_handler):
627 request_handler.response.out.write('<html><body>')
628 request_handler.response.out.write(_safe_html(self._message))
629 request_handler.response.out.write('</body></html>')
630
631 def oauth_required(self, method):
632 """Decorator that starts the OAuth 2.0 dance.
633
634 Starts the OAuth dance for the logged in user if they haven't already
635 granted access for this application.
636
637 Args:
638 method: callable, to be decorated method of a webapp.RequestHandler
639 instance.
640 """
641
642 def check_oauth(request_handler, *args, **kwargs):
643 if self._in_error:
644 self._display_error_message(request_handler)
645 return
646
647 user = users.get_current_user()
648 # Don't use @login_decorator as this could be used in a
649 # POST request.
650 if not user:
651 request_handler.redirect(users.create_login_url(
652 request_handler.request.uri))
653 return
654
655 self._create_flow(request_handler)
656
657 # Store the request URI in 'state' so we can use it later
658 self.flow.params['state'] = _build_state_value(
659 request_handler, user)
660 self.credentials = self._storage_class(
661 self._credentials_class, None,
662 self._credentials_property_name, user=user).get()
663
664 if not self.has_credentials():
665 return request_handler.redirect(self.authorize_url())
666 try:
667 resp = method(request_handler, *args, **kwargs)
668 except AccessTokenRefreshError:
669 return request_handler.redirect(self.authorize_url())
670 finally:
671 self.credentials = None
672 return resp
673
674 return check_oauth
675
676 def _create_flow(self, request_handler):
677 """Create the Flow object.
678
679 The Flow is calculated lazily since we don't know where this app is
680 running until it receives a request, at which point redirect_uri can be
681 calculated and then the Flow object can be constructed.
682
683 Args:
684 request_handler: webapp.RequestHandler, the request handler.
685 """
686 if self.flow is None:
687 redirect_uri = request_handler.request.relative_url(
688 self._callback_path) # Usually /oauth2callback
689 self.flow = OAuth2WebServerFlow(
690 self._client_id, self._client_secret, self._scope,
691 redirect_uri=redirect_uri, user_agent=self._user_agent,
692 auth_uri=self._auth_uri, token_uri=self._token_uri,
693 revoke_uri=self._revoke_uri, **self._kwargs)
694
695 def oauth_aware(self, method):
696 """Decorator that sets up for OAuth 2.0 dance, but doesn't do it.
697
698 Does all the setup for the OAuth dance, but doesn't initiate it.
699 This decorator is useful if you want to create a page that knows
700 whether or not the user has granted access to this application.
701 From within a method decorated with @oauth_aware the has_credentials()
702 and authorize_url() methods can be called.
703
704 Args:
705 method: callable, to be decorated method of a webapp.RequestHandler
706 instance.
707 """
708
709 def setup_oauth(request_handler, *args, **kwargs):
710 if self._in_error:
711 self._display_error_message(request_handler)
712 return
713
714 user = users.get_current_user()
715 # Don't use @login_decorator as this could be used in a
716 # POST request.
717 if not user:
718 request_handler.redirect(users.create_login_url(
719 request_handler.request.uri))
720 return
721
722 self._create_flow(request_handler)
723
724 self.flow.params['state'] = _build_state_value(request_handler,
725 user)
726 self.credentials = self._storage_class(
727 self._credentials_class, None,
728 self._credentials_property_name, user=user).get()
729 try:
730 resp = method(request_handler, *args, **kwargs)
731 finally:
732 self.credentials = None
733 return resp
734 return setup_oauth
735
736 def has_credentials(self):
737 """True if for the logged in user there are valid access Credentials.
738
739 Must only be called from with a webapp.RequestHandler subclassed method
740 that had been decorated with either @oauth_required or @oauth_aware.
741 """
742 return self.credentials is not None and not self.credentials.invalid
743
744 def authorize_url(self):
745 """Returns the URL to start the OAuth dance.
746
747 Must only be called from with a webapp.RequestHandler subclassed method
748 that had been decorated with either @oauth_required or @oauth_aware.
749 """
750 url = self.flow.step1_get_authorize_url()
751 return str(url)
752
753 def http(self, *args, **kwargs):
754 """Returns an authorized http instance.
755
756 Must only be called from within an @oauth_required decorated method, or
757 from within an @oauth_aware decorated method where has_credentials()
758 returns True.
759
760 Args:
761 *args: Positional arguments passed to httplib2.Http constructor.
762 **kwargs: Positional arguments passed to httplib2.Http constructor.
763 """
764 return self.credentials.authorize(httplib2.Http(*args, **kwargs))
765
766 @property
767 def callback_path(self):
768 """The absolute path where the callback will occur.
769
770 Note this is the absolute path, not the absolute URI, that will be
771 calculated by the decorator at runtime. See callback_handler() for how
772 this should be used.
773
774 Returns:
775 The callback path as a string.
776 """
777 return self._callback_path
778
779 def callback_handler(self):
780 """RequestHandler for the OAuth 2.0 redirect callback.
781
782 Usage::
783
784 app = webapp.WSGIApplication([
785 ('/index', MyIndexHandler),
786 ...,
787 (decorator.callback_path, decorator.callback_handler())
788 ])
789
790 Returns:
791 A webapp.RequestHandler that handles the redirect back from the
792 server during the OAuth 2.0 dance.
793 """
794 decorator = self
795
796 class OAuth2Handler(webapp.RequestHandler):
797 """Handler for the redirect_uri of the OAuth 2.0 dance."""
798
799 @login_required
800 def get(self):
801 error = self.request.get('error')
802 if error:
803 errormsg = self.request.get('error_description', error)
804 self.response.out.write(
805 'The authorization request failed: %s' %
806 _safe_html(errormsg))
807 else:
808 user = users.get_current_user()
809 decorator._create_flow(self)
810 credentials = decorator.flow.step2_exchange(
811 self.request.params)
812 decorator._storage_class(
813 decorator._credentials_class, None,
814 decorator._credentials_property_name,
815 user=user).put(credentials)
816 redirect_uri = _parse_state_value(
817 str(self.request.get('state')), user)
818
819 if (decorator._token_response_param and
820 credentials.token_response):
821 resp_json = json.dumps(credentials.token_response)
822 redirect_uri = util._add_query_parameter(
823 redirect_uri, decorator._token_response_param,
824 resp_json)
825
826 self.redirect(redirect_uri)
827
828 return OAuth2Handler
829
830 def callback_application(self):
831 """WSGI application for handling the OAuth 2.0 redirect callback.
832
833 If you need finer grained control use `callback_handler` which returns
834 just the webapp.RequestHandler.
835
836 Returns:
837 A webapp.WSGIApplication that handles the redirect back from the
838 server during the OAuth 2.0 dance.
839 """
840 return webapp.WSGIApplication([
841 (self.callback_path, self.callback_handler())
842 ])
843
844
845 class OAuth2DecoratorFromClientSecrets(OAuth2Decorator):
846 """An OAuth2Decorator that builds from a clientsecrets file.
847
848 Uses a clientsecrets file as the source for all the information when
849 constructing an OAuth2Decorator.
850
851 ::
852
853 decorator = OAuth2DecoratorFromClientSecrets(
854 os.path.join(os.path.dirname(__file__), 'client_secrets.json')
855 scope='https://www.googleapis.com/auth/plus')
856
857 class MainHandler(webapp.RequestHandler):
858 @decorator.oauth_required
859 def get(self):
860 http = decorator.http()
861 # http is authorized with the user's Credentials and can be
862 # used in API calls
863
864 """
865
866 @util.positional(3)
867 def __init__(self, filename, scope, message=None, cache=None, **kwargs):
868 """Constructor
869
870 Args:
871 filename: string, File name of client secrets.
872 scope: string or iterable of strings, scope(s) of the credentials
873 being requested.
874 message: string, A friendly string to display to the user if the
875 clientsecrets file is missing or invalid. The message may
876 contain HTML and will be presented on the web interface
877 for any method that uses the decorator.
878 cache: An optional cache service client that implements get() and
879 set()
880 methods. See clientsecrets.loadfile() for details.
881 **kwargs: dict, Keyword arguments are passed along as kwargs to
882 the OAuth2WebServerFlow constructor.
883 """
884 client_type, client_info = clientsecrets.loadfile(filename,
885 cache=cache)
886 if client_type not in (clientsecrets.TYPE_WEB,
887 clientsecrets.TYPE_INSTALLED):
888 raise InvalidClientSecretsError(
889 "OAuth2Decorator doesn't support this OAuth 2.0 flow.")
890
891 constructor_kwargs = dict(kwargs)
892 constructor_kwargs.update({
893 'auth_uri': client_info['auth_uri'],
894 'token_uri': client_info['token_uri'],
895 'message': message,
896 })
897 revoke_uri = client_info.get('revoke_uri')
898 if revoke_uri is not None:
899 constructor_kwargs['revoke_uri'] = revoke_uri
900 super(OAuth2DecoratorFromClientSecrets, self).__init__(
901 client_info['client_id'], client_info['client_secret'],
902 scope, **constructor_kwargs)
903 if message is not None:
904 self._message = message
905 else:
906 self._message = 'Please configure your application for OAuth 2.0.'
907
908
909 @util.positional(2)
910 def oauth2decorator_from_clientsecrets(filename, scope,
911 message=None, cache=None):
912 """Creates an OAuth2Decorator populated from a clientsecrets file.
913
914 Args:
915 filename: string, File name of client secrets.
916 scope: string or list of strings, scope(s) of the credentials being
917 requested.
918 message: string, A friendly string to display to the user if the
919 clientsecrets file is missing or invalid. The message may
920 contain HTML and will be presented on the web interface for
921 any method that uses the decorator.
922 cache: An optional cache service client that implements get() and set()
923 methods. See clientsecrets.loadfile() for details.
924
925 Returns: An OAuth2Decorator
926 """
927 return OAuth2DecoratorFromClientSecrets(filename, scope,
928 message=message, cache=cache)
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698