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

Side by Side Diff: appengine/chromium_build_logs/third_party/oauth2client/flask_util.py

Issue 1260293009: make version of ts_mon compatible with appengine (Closed) Base URL: https://chromium.googlesource.com/infra/infra.git@master
Patch Set: clean up code Created 5 years, 4 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 2015 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 the Flask web framework
16
17 Provides a Flask extension that makes using OAuth2 web server flow easier.
18 The extension includes views that handle the entire auth flow and a @required
19 decorator to automatically ensure that user credentials are available.
20
21 To configure::
22
23 from oauth2client.flask_util import UserOAuth2
24
25 app = Flask(__name__)
26
27 app.config['SECRET_KEY'] = 'your-secret-key'
28
29 app.config['OAUTH2_CLIENT_SECRETS_JSON'] = 'client_secrets.json'
30 # or, specify the client id and secret separately
31 app.config['OAUTH2_CLIENT_ID'] = 'your-client-id'
32 app.config['OAUTH2_CLIENT_SECRET'] = 'your-client-secret'
33
34 oauth2 = UserOAuth2(app)
35
36
37 To use::
38
39 # Note that app.route should be the outermost decorator.
40 @app.route('/needs_credentials')
41 @oauth2.required
42 def example():
43 # http is authorized with the user's credentials and can be used
44 # to make http calls.
45 http = oauth2.http()
46
47 # Or, you can access the credentials directly
48 credentials = oauth2.credentials
49
50
51 @app.route('/info')
52 @oauth2.required
53 def info():
54 return "Hello, {}".format(oauth2.email)
55
56 @app.route('/optional')
57 def optional():
58 if oauth2.has_credentials():
59 return 'Credentials found!'
60 else:
61 return 'No credentials!'
62
63 """
64
65 __author__ = 'jonwayne@google.com (Jon Wayne Parrott)'
66
67 import hashlib
68 import json
69 import os
70 from functools import wraps
71
72 import six.moves.http_client as httplib
73 import httplib2
74
75 try:
76 from flask import Blueprint
77 from flask import _app_ctx_stack
78 from flask import current_app
79 from flask import redirect
80 from flask import request
81 from flask import session
82 from flask import url_for
83 except ImportError:
84 raise ImportError('The flask utilities require flask 0.9 or newer.')
85
86 from oauth2client.client import FlowExchangeError
87 from oauth2client.client import OAuth2Credentials
88 from oauth2client.client import OAuth2WebServerFlow
89 from oauth2client.client import Storage
90 from oauth2client import clientsecrets
91 from oauth2client import util
92
93
94 DEFAULT_SCOPES = ('email',)
95
96
97 class UserOAuth2(object):
98 """Flask extension for making OAuth 2.0 easier.
99
100 Configuration values:
101 * GOOGLE_OAUTH2_CLIENT_SECRETS_JSON path to a client secrets json file,
102 obtained from the credentials screen in the Google Developers
103 console.
104 * GOOGLE_OAUTH2_CLIENT_ID the oauth2 credentials' client ID. This is
105 only needed if OAUTH2_CLIENT_SECRETS_JSON is not specified.
106 * GOOGLE_OAUTH2_CLIENT_SECRET the oauth2 credentials' client secret.
107 This is only needed if OAUTH2_CLIENT_SECRETS_JSON is not specified.
108
109 If app is specified, all arguments will be passed along to init_app.
110
111 If no app is specified, then you should call init_app in your application
112 factory to finish initialization.
113 """
114
115 def __init__(self, app=None, *args, **kwargs):
116 self.app = app
117 if app is not None:
118 self.init_app(app, *args, **kwargs)
119
120 def init_app(self, app, scopes=None, client_secrets_file=None,
121 client_id=None, client_secret=None, authorize_callback=None,
122 storage=None, **kwargs):
123 """Initialize this extension for the given app.
124
125 Arguments:
126 app: A Flask application.
127 scopes: Optional list of scopes to authorize.
128 client_secrets_file: Path to a file containing client secrets. You
129 can also specify the OAUTH2_CLIENT_SECRETS_JSON config value.
130 client_id: If not specifying a client secrets file, specify the
131 OAuth2 client id. You can also specify the
132 GOOGLE_OAUTH2_CLIENT_ID config value. You must also provide a
133 client secret.
134 client_secret: The OAuth2 client secret. You can also specify the
135 GOOGLE_OAUTH2_CLIENT_SECRET config value.
136 authorize_callback: A function that is executed after successful
137 user authorization.
138 storage: A oauth2client.client.Storage subclass for storing the
139 credentials. By default, this is a Flask session based storage.
140 kwargs: Any additional args are passed along to the Flow
141 constructor.
142 """
143 self.app = app
144 self.authorize_callback = authorize_callback
145 self.flow_kwargs = kwargs
146
147 if storage is None:
148 storage = FlaskSessionStorage()
149 self.storage = storage
150
151 if scopes is None:
152 scopes = app.config.get('GOOGLE_OAUTH2_SCOPES', DEFAULT_SCOPES)
153 self.scopes = scopes
154
155 self._load_config(client_secrets_file, client_id, client_secret)
156
157 app.register_blueprint(self._create_blueprint())
158
159 def _load_config(self, client_secrets_file, client_id, client_secret):
160 """Loads oauth2 configuration in order of priority.
161
162 Priority:
163 1. Config passed to the constructor or init_app.
164 2. Config passed via the GOOGLE_OAUTH2_CLIENT_SECRETS_FILE app
165 config.
166 3. Config passed via the GOOGLE_OAUTH2_CLIENT_ID and
167 GOOGLE_OAUTH2_CLIENT_SECRET app config.
168
169 Raises:
170 ValueError if no config could be found.
171 """
172 if client_id and client_secret:
173 self.client_id, self.client_secret = client_id, client_secret
174 return
175
176 if client_secrets_file:
177 self._load_client_secrets(client_secrets_file)
178 return
179
180 if 'GOOGLE_OAUTH2_CLIENT_SECRETS_FILE' in self.app.config:
181 self._load_client_secrets(
182 self.app.config['GOOGLE_OAUTH2_CLIENT_SECRETS_FILE'])
183 return
184
185 try:
186 self.client_id, self.client_secret = (
187 self.app.config['GOOGLE_OAUTH2_CLIENT_ID'],
188 self.app.config['GOOGLE_OAUTH2_CLIENT_SECRET'])
189 except KeyError:
190 raise ValueError(
191 'OAuth2 configuration could not be found. Either specify the '
192 'client_secrets_file or client_id and client_secret or set the'
193 'app configuration variables GOOGLE_OAUTH2_CLIENT_SECRETS_FILE '
194 'or GOOGLE_OAUTH2_CLIENT_ID and GOOGLE_OAUTH2_CLIENT_SECRET.')
195
196 def _load_client_secrets(self, filename):
197 """Loads client secrets from the given filename."""
198 client_type, client_info = clientsecrets.loadfile(filename)
199 if client_type != clientsecrets.TYPE_WEB:
200 raise ValueError(
201 'The flow specified in %s is not supported.' % client_type)
202
203 self.client_id = client_info['client_id']
204 self.client_secret = client_info['client_secret']
205
206 def _make_flow(self, return_url=None, **kwargs):
207 """Creates a Web Server Flow"""
208 # Generate a CSRF token to prevent malicious requests.
209 csrf_token = hashlib.sha256(os.urandom(1024)).hexdigest()
210
211 session['google_oauth2_csrf_token'] = csrf_token
212
213 state = json.dumps({
214 'csrf_token': csrf_token,
215 'return_url': return_url
216 })
217
218 kw = self.flow_kwargs.copy()
219 kw.update(kwargs)
220
221 extra_scopes = util.scopes_to_string(kw.pop('scopes', ''))
222 scopes = ' '.join([util.scopes_to_string(self.scopes), extra_scopes])
223
224 return OAuth2WebServerFlow(
225 client_id=self.client_id,
226 client_secret=self.client_secret,
227 scope=scopes,
228 state=state,
229 redirect_uri=url_for('oauth2.callback', _external=True),
230 **kw)
231
232 def _create_blueprint(self):
233 bp = Blueprint('oauth2', __name__)
234 bp.add_url_rule('/oauth2authorize', 'authorize', self.authorize_view)
235 bp.add_url_rule('/oauth2callback', 'callback', self.callback_view)
236
237 return bp
238
239 def authorize_view(self):
240 """Flask view that starts the authorization flow by redirecting the
241 user to the OAuth2 provider."""
242 args = request.args.to_dict()
243
244 return_url = args.pop('return_url', None)
245 if return_url is None:
246 return_url = request.referrer or '/'
247
248 flow = self._make_flow(return_url=return_url, **args)
249 auth_url = flow.step1_get_authorize_url()
250
251 return redirect(auth_url)
252
253 def callback_view(self):
254 """Flask view that handles the user's return from the OAuth2 provider
255 and exchanges the authorization code for credentials and stores the
256 credentials."""
257 if 'error' in request.args:
258 reason = request.args.get(
259 'error_description', request.args.get('error', ''))
260 return 'Authorization failed: %s' % reason, httplib.BAD_REQUEST
261
262 try:
263 encoded_state = request.args['state']
264 server_csrf = session['google_oauth2_csrf_token']
265 code = request.args['code']
266 except KeyError:
267 return 'Invalid request', httplib.BAD_REQUEST
268
269 try:
270 state = json.loads(encoded_state)
271 client_csrf = state['csrf_token']
272 return_url = state['return_url']
273 except (ValueError, KeyError):
274 return 'Invalid request state', httplib.BAD_REQUEST
275
276 if client_csrf != server_csrf:
277 return 'Invalid request state', httplib.BAD_REQUEST
278
279 flow = self._make_flow()
280
281 # Exchange the auth code for credentials.
282 try:
283 credentials = flow.step2_exchange(code)
284 except FlowExchangeError as exchange_error:
285 current_app.logger.exception(exchange_error)
286 return 'An error occurred: %s' % exchange_error, httplib.BAD_REQUEST
287
288 # Save the credentials to the storage.
289 self.storage.put(credentials)
290
291 if self.authorize_callback:
292 self.authorize_callback(credentials)
293
294 return redirect(return_url)
295
296 @property
297 def credentials(self):
298 """The credentials for the current user or None if unavailable."""
299 ctx = _app_ctx_stack.top
300
301 if not hasattr(ctx, 'google_oauth2_credentials'):
302 ctx.google_oauth2_credentials = self.storage.get()
303
304 return ctx.google_oauth2_credentials
305
306 def has_credentials(self):
307 """Returns True if there are valid credentials for the current user."""
308 return self.credentials and not self.credentials.invalid
309
310 @property
311 def email(self):
312 """Returns the user's email address or None if there are no credentials.
313
314 The email address is provided by the current credentials' id_token. This
315 should not be used as unique identifier as the user can change their
316 email. If you need a unique identifier, use user_id.
317 """
318 if not self.credentials:
319 return None
320 try:
321 return self.credentials.id_token['email']
322 except KeyError:
323 current_app.logger.error(
324 'Invalid id_token %s', self.credentials.id_token)
325
326 @property
327 def user_id(self):
328 """Returns the a unique identifier for the user or None if there are no
329 credentials.
330
331 The id is provided by the current credentials' id_token.
332 """
333 if not self.credentials:
334 return None
335 try:
336 return self.credentials.id_token['sub']
337 except KeyError:
338 current_app.logger.error(
339 'Invalid id_token %s', self.credentials.id_token)
340
341 def authorize_url(self, return_url, **kwargs):
342 """Creates a URL that can be used to start the authorization flow.
343
344 When the user is directed to the URL, the authorization flow will begin.
345 Once complete, the user will be redirected to the specified return URL.
346
347 Any kwargs are passed into the flow constructor.
348 """
349 return url_for('oauth2.authorize', return_url=return_url, **kwargs)
350
351 def required(self, decorated_function=None, **decorator_kwargs):
352 """Decorator to require OAuth2 credentials for a view.
353
354 If credentials are not available for the current user, then they will
355 be redirected to the authorization flow. Once complete, the user will
356 be redirected back to the original page.
357 """
358 def curry_wrapper(wrapped_function):
359 @wraps(wrapped_function)
360 def required_wrapper(*args, **kwargs):
361 if not self.has_credentials():
362 if 'return_url' not in decorator_kwargs:
363 decorator_kwargs['return_url'] = request.url
364 return redirect(self.authorize_url(**decorator_kwargs))
365 else:
366 return wrapped_function(*args, **kwargs)
367 return required_wrapper
368
369 if decorated_function:
370 return curry_wrapper(decorated_function)
371 else:
372 return curry_wrapper
373
374 def http(self, *args, **kwargs):
375 """Returns an authorized http instance.
376
377 Can only be called if there are valid credentials for the user, such
378 as inside of a view that is decorated with @required.
379
380 Args:
381 *args: Positional arguments passed to httplib2.Http constructor.
382 **kwargs: Positional arguments passed to httplib2.Http constructor.
383
384 Raises:
385 ValueError if no credentials are available.
386 """
387 if not self.credentials:
388 raise ValueError('No credentials available.')
389 return self.credentials.authorize(httplib2.Http(*args, **kwargs))
390
391
392 class FlaskSessionStorage(Storage):
393 """Storage implementation that uses Flask sessions.
394
395 Note that flask's default sessions are signed but not encrypted. Users
396 can see their own credentials and non-https connections can intercept user
397 credentials. We strongly recommend using a server-side session
398 implementation.
399 """
400 def locked_get(self):
401 serialized = session.get('google_oauth2_credentials')
402
403 if serialized is None:
404 return None
405
406 credentials = OAuth2Credentials.from_json(serialized)
407
408 if credentials:
409 credentials.set_store(self)
410
411 return credentials
412
413 def locked_put(self, credentials):
414 session['google_oauth2_credentials'] = credentials.to_json()
415
416 def locked_delete(self):
417 if 'google_oauth2_credentials' in session:
418 del session['google_oauth2_credentials']
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698