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

Side by Side Diff: third_party/google-endpoints/oauth2client/flask_util.py

Issue 2666783008: Add google-endpoints to third_party/. (Closed)
Patch Set: Created 3 years, 10 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
19 ``@required`` decorator to automatically ensure that user credentials are
20 available.
21
22
23 Configuration
24 =============
25
26 To configure, you'll need a set of OAuth2 web application credentials from the
27 `Google Developer's Console <https://console.developers.google.com/project/_/\
28 apiui/credential>`__.
29
30 .. code-block:: python
31
32 from oauth2client.flask_util import UserOAuth2
33
34 app = Flask(__name__)
35
36 app.config['SECRET_KEY'] = 'your-secret-key'
37
38 app.config['GOOGLE_OAUTH2_CLIENT_SECRETS_JSON'] = 'client_secrets.json'
39
40 # or, specify the client id and secret separately
41 app.config['GOOGLE_OAUTH2_CLIENT_ID'] = 'your-client-id'
42 app.config['GOOGLE_OAUTH2_CLIENT_SECRET'] = 'your-client-secret'
43
44 oauth2 = UserOAuth2(app)
45
46
47 Usage
48 =====
49
50 Once configured, you can use the :meth:`UserOAuth2.required` decorator to
51 ensure that credentials are available within a view.
52
53 .. code-block:: python
54 :emphasize-lines: 3,7,10
55
56 # Note that app.route should be the outermost decorator.
57 @app.route('/needs_credentials')
58 @oauth2.required
59 def example():
60 # http is authorized with the user's credentials and can be used
61 # to make http calls.
62 http = oauth2.http()
63
64 # Or, you can access the credentials directly
65 credentials = oauth2.credentials
66
67 If you want credentials to be optional for a view, you can leave the decorator
68 off and use :meth:`UserOAuth2.has_credentials` to check.
69
70 .. code-block:: python
71 :emphasize-lines: 3
72
73 @app.route('/optional')
74 def optional():
75 if oauth2.has_credentials():
76 return 'Credentials found!'
77 else:
78 return 'No credentials!'
79
80
81 When credentials are available, you can use :attr:`UserOAuth2.email` and
82 :attr:`UserOAuth2.user_id` to access information from the `ID Token
83 <https://developers.google.com/identity/protocols/OpenIDConnect?hl=en>`__, if
84 available.
85
86 .. code-block:: python
87 :emphasize-lines: 4
88
89 @app.route('/info')
90 @oauth2.required
91 def info():
92 return "Hello, {} ({})".format(oauth2.email, oauth2.user_id)
93
94
95 URLs & Trigging Authorization
96 =============================
97
98 The extension will add two new routes to your application:
99
100 * ``"oauth2.authorize"`` -> ``/oauth2authorize``
101 * ``"oauth2.callback"`` -> ``/oauth2callback``
102
103 When configuring your OAuth2 credentials on the Google Developer's Console, be
104 sure to add ``http[s]://[your-app-url]/oauth2callback`` as an authorized
105 callback url.
106
107 Typically you don't not need to use these routes directly, just be sure to
108 decorate any views that require credentials with ``@oauth2.required``. If
109 needed, you can trigger authorization at any time by redirecting the user
110 to the URL returned by :meth:`UserOAuth2.authorize_url`.
111
112 .. code-block:: python
113 :emphasize-lines: 3
114
115 @app.route('/login')
116 def login():
117 return oauth2.authorize_url("/")
118
119
120 Incremental Auth
121 ================
122
123 This extension also supports `Incremental Auth <https://developers.google.com\
124 /identity/protocols/OAuth2WebServer?hl=en#incrementalAuth>`__. To enable it,
125 configure the extension with ``include_granted_scopes``.
126
127 .. code-block:: python
128
129 oauth2 = UserOAuth2(app, include_granted_scopes=True)
130
131 Then specify any additional scopes needed on the decorator, for example:
132
133 .. code-block:: python
134 :emphasize-lines: 2,7
135
136 @app.route('/drive')
137 @oauth2.required(scopes=["https://www.googleapis.com/auth/drive"])
138 def requires_drive():
139 ...
140
141 @app.route('/calendar')
142 @oauth2.required(scopes=["https://www.googleapis.com/auth/calendar"])
143 def requires_calendar():
144 ...
145
146 The decorator will ensure that the the user has authorized all specified scopes
147 before allowing them to access the view, and will also ensure that credentials
148 do not lose any previously authorized scopes.
149
150
151 Storage
152 =======
153
154 By default, the extension uses a Flask session-based storage solution. This
155 means that credentials are only available for the duration of a session. It
156 also means that with Flask's default configuration, the credentials will be
157 visible in the session cookie. It's highly recommended to use database-backed
158 session and to use https whenever handling user credentials.
159
160 If you need the credentials to be available longer than a user session or
161 available outside of a request context, you will need to implement your own
162 :class:`oauth2client.Storage`.
163 """
164
165 import hashlib
166 import json
167 import os
168 import pickle
169 from functools import wraps
170
171 import six.moves.http_client as httplib
172 import httplib2
173
174 try:
175 from flask import Blueprint
176 from flask import _app_ctx_stack
177 from flask import current_app
178 from flask import redirect
179 from flask import request
180 from flask import session
181 from flask import url_for
182 except ImportError: # pragma: NO COVER
183 raise ImportError('The flask utilities require flask 0.9 or newer.')
184
185 from oauth2client.client import FlowExchangeError
186 from oauth2client.client import OAuth2Credentials
187 from oauth2client.client import OAuth2WebServerFlow
188 from oauth2client.client import Storage
189 from oauth2client import clientsecrets
190
191
192 __author__ = 'jonwayne@google.com (Jon Wayne Parrott)'
193
194 _DEFAULT_SCOPES = ('email',)
195 _CREDENTIALS_KEY = 'google_oauth2_credentials'
196 _FLOW_KEY = 'google_oauth2_flow_{0}'
197 _CSRF_KEY = 'google_oauth2_csrf_token'
198
199
200 def _get_flow_for_token(csrf_token):
201 """Retrieves the flow instance associated with a given CSRF token from
202 the Flask session."""
203 flow_pickle = session.get(
204 _FLOW_KEY.format(csrf_token), None)
205
206 if flow_pickle is None:
207 return None
208 else:
209 return pickle.loads(flow_pickle)
210
211
212 class UserOAuth2(object):
213 """Flask extension for making OAuth 2.0 easier.
214
215 Configuration values:
216
217 * ``GOOGLE_OAUTH2_CLIENT_SECRETS_JSON`` path to a client secrets json
218 file, obtained from the credentials screen in the Google Developers
219 console.
220 * ``GOOGLE_OAUTH2_CLIENT_ID`` the oauth2 credentials' client ID. This
221 is only needed if ``GOOGLE_OAUTH2_CLIENT_SECRETS_JSON`` is not specifi ed.
222 * ``GOOGLE_OAUTH2_CLIENT_SECRET`` the oauth2 credentials' client
223 secret. This is only needed if ``GOOGLE_OAUTH2_CLIENT_SECRETS_JSON`` i s not
224 specified.
225
226 If app is specified, all arguments will be passed along to init_app.
227
228 If no app is specified, then you should call init_app in your application
229 factory to finish initialization.
230 """
231
232 def __init__(self, app=None, *args, **kwargs):
233 self.app = app
234 if app is not None:
235 self.init_app(app, *args, **kwargs)
236
237 def init_app(self, app, scopes=None, client_secrets_file=None,
238 client_id=None, client_secret=None, authorize_callback=None,
239 storage=None, **kwargs):
240 """Initialize this extension for the given app.
241
242 Arguments:
243 app: A Flask application.
244 scopes: Optional list of scopes to authorize.
245 client_secrets_file: Path to a file containing client secrets. You
246 can also specify the GOOGLE_OAUTH2_CLIENT_SECRETS_JSON config va lue.
247 client_id: If not specifying a client secrets file, specify the
248 OAuth2 client id. You can also specify the
249 GOOGLE_OAUTH2_CLIENT_ID config value. You must also provide a
250 client secret.
251 client_secret: The OAuth2 client secret. You can also specify the
252 GOOGLE_OAUTH2_CLIENT_SECRET config value.
253 authorize_callback: A function that is executed after successful
254 user authorization.
255 storage: A oauth2client.client.Storage subclass for storing the
256 credentials. By default, this is a Flask session based storage.
257 kwargs: Any additional args are passed along to the Flow
258 constructor.
259 """
260 self.app = app
261 self.authorize_callback = authorize_callback
262 self.flow_kwargs = kwargs
263
264 if storage is None:
265 storage = FlaskSessionStorage()
266 self.storage = storage
267
268 if scopes is None:
269 scopes = app.config.get('GOOGLE_OAUTH2_SCOPES', _DEFAULT_SCOPES)
270 self.scopes = scopes
271
272 self._load_config(client_secrets_file, client_id, client_secret)
273
274 app.register_blueprint(self._create_blueprint())
275
276 def _load_config(self, client_secrets_file, client_id, client_secret):
277 """Loads oauth2 configuration in order of priority.
278
279 Priority:
280 1. Config passed to the constructor or init_app.
281 2. Config passed via the GOOGLE_OAUTH2_CLIENT_SECRETS_FILE app
282 config.
283 3. Config passed via the GOOGLE_OAUTH2_CLIENT_ID and
284 GOOGLE_OAUTH2_CLIENT_SECRET app config.
285
286 Raises:
287 ValueError if no config could be found.
288 """
289 if client_id and client_secret:
290 self.client_id, self.client_secret = client_id, client_secret
291 return
292
293 if client_secrets_file:
294 self._load_client_secrets(client_secrets_file)
295 return
296
297 if 'GOOGLE_OAUTH2_CLIENT_SECRETS_FILE' in self.app.config:
298 self._load_client_secrets(
299 self.app.config['GOOGLE_OAUTH2_CLIENT_SECRETS_FILE'])
300 return
301
302 try:
303 self.client_id, self.client_secret = (
304 self.app.config['GOOGLE_OAUTH2_CLIENT_ID'],
305 self.app.config['GOOGLE_OAUTH2_CLIENT_SECRET'])
306 except KeyError:
307 raise ValueError(
308 'OAuth2 configuration could not be found. Either specify the '
309 'client_secrets_file or client_id and client_secret or set the'
310 'app configuration variables '
311 'GOOGLE_OAUTH2_CLIENT_SECRETS_FILE or '
312 'GOOGLE_OAUTH2_CLIENT_ID and GOOGLE_OAUTH2_CLIENT_SECRET.')
313
314 def _load_client_secrets(self, filename):
315 """Loads client secrets from the given filename."""
316 client_type, client_info = clientsecrets.loadfile(filename)
317 if client_type != clientsecrets.TYPE_WEB:
318 raise ValueError(
319 'The flow specified in {0} is not supported.'.format(
320 client_type))
321
322 self.client_id = client_info['client_id']
323 self.client_secret = client_info['client_secret']
324
325 def _make_flow(self, return_url=None, **kwargs):
326 """Creates a Web Server Flow"""
327 # Generate a CSRF token to prevent malicious requests.
328 csrf_token = hashlib.sha256(os.urandom(1024)).hexdigest()
329
330 session[_CSRF_KEY] = csrf_token
331
332 state = json.dumps({
333 'csrf_token': csrf_token,
334 'return_url': return_url
335 })
336
337 kw = self.flow_kwargs.copy()
338 kw.update(kwargs)
339
340 extra_scopes = kw.pop('scopes', [])
341 scopes = set(self.scopes).union(set(extra_scopes))
342
343 flow = OAuth2WebServerFlow(
344 client_id=self.client_id,
345 client_secret=self.client_secret,
346 scope=scopes,
347 state=state,
348 redirect_uri=url_for('oauth2.callback', _external=True),
349 **kw)
350
351 flow_key = _FLOW_KEY.format(csrf_token)
352 session[flow_key] = pickle.dumps(flow)
353
354 return flow
355
356 def _create_blueprint(self):
357 bp = Blueprint('oauth2', __name__)
358 bp.add_url_rule('/oauth2authorize', 'authorize', self.authorize_view)
359 bp.add_url_rule('/oauth2callback', 'callback', self.callback_view)
360
361 return bp
362
363 def authorize_view(self):
364 """Flask view that starts the authorization flow.
365
366 Starts flow by redirecting the user to the OAuth2 provider.
367 """
368 args = request.args.to_dict()
369
370 # Scopes will be passed as mutliple args, and to_dict() will only
371 # return one. So, we use getlist() to get all of the scopes.
372 args['scopes'] = request.args.getlist('scopes')
373
374 return_url = args.pop('return_url', None)
375 if return_url is None:
376 return_url = request.referrer or '/'
377
378 flow = self._make_flow(return_url=return_url, **args)
379 auth_url = flow.step1_get_authorize_url()
380
381 return redirect(auth_url)
382
383 def callback_view(self):
384 """Flask view that handles the user's return from OAuth2 provider.
385
386 On return, exchanges the authorization code for credentials and stores
387 the credentials.
388 """
389 if 'error' in request.args:
390 reason = request.args.get(
391 'error_description', request.args.get('error', ''))
392 return ('Authorization failed: {0}'.format(reason),
393 httplib.BAD_REQUEST)
394
395 try:
396 encoded_state = request.args['state']
397 server_csrf = session[_CSRF_KEY]
398 code = request.args['code']
399 except KeyError:
400 return 'Invalid request', httplib.BAD_REQUEST
401
402 try:
403 state = json.loads(encoded_state)
404 client_csrf = state['csrf_token']
405 return_url = state['return_url']
406 except (ValueError, KeyError):
407 return 'Invalid request state', httplib.BAD_REQUEST
408
409 if client_csrf != server_csrf:
410 return 'Invalid request state', httplib.BAD_REQUEST
411
412 flow = _get_flow_for_token(server_csrf)
413
414 if flow is None:
415 return 'Invalid request state', httplib.BAD_REQUEST
416
417 # Exchange the auth code for credentials.
418 try:
419 credentials = flow.step2_exchange(code)
420 except FlowExchangeError as exchange_error:
421 current_app.logger.exception(exchange_error)
422 content = 'An error occurred: {0}'.format(exchange_error)
423 return content, httplib.BAD_REQUEST
424
425 # Save the credentials to the storage.
426 self.storage.put(credentials)
427
428 if self.authorize_callback:
429 self.authorize_callback(credentials)
430
431 return redirect(return_url)
432
433 @property
434 def credentials(self):
435 """The credentials for the current user or None if unavailable."""
436 ctx = _app_ctx_stack.top
437
438 if not hasattr(ctx, _CREDENTIALS_KEY):
439 ctx.google_oauth2_credentials = self.storage.get()
440
441 return ctx.google_oauth2_credentials
442
443 def has_credentials(self):
444 """Returns True if there are valid credentials for the current user."""
445 return self.credentials and not self.credentials.invalid
446
447 @property
448 def email(self):
449 """Returns the user's email address or None if there are no credentials.
450
451 The email address is provided by the current credentials' id_token.
452 This should not be used as unique identifier as the user can change
453 their email. If you need a unique identifier, use user_id.
454 """
455 if not self.credentials:
456 return None
457 try:
458 return self.credentials.id_token['email']
459 except KeyError:
460 current_app.logger.error(
461 'Invalid id_token {0}'.format(self.credentials.id_token))
462
463 @property
464 def user_id(self):
465 """Returns the a unique identifier for the user
466
467 Returns None if there are no credentials.
468
469 The id is provided by the current credentials' id_token.
470 """
471 if not self.credentials:
472 return None
473 try:
474 return self.credentials.id_token['sub']
475 except KeyError:
476 current_app.logger.error(
477 'Invalid id_token {0}'.format(self.credentials.id_token))
478
479 def authorize_url(self, return_url, **kwargs):
480 """Creates a URL that can be used to start the authorization flow.
481
482 When the user is directed to the URL, the authorization flow will
483 begin. Once complete, the user will be redirected to the specified
484 return URL.
485
486 Any kwargs are passed into the flow constructor.
487 """
488 return url_for('oauth2.authorize', return_url=return_url, **kwargs)
489
490 def required(self, decorated_function=None, scopes=None,
491 **decorator_kwargs):
492 """Decorator to require OAuth2 credentials for a view.
493
494 If credentials are not available for the current user, then they will
495 be redirected to the authorization flow. Once complete, the user will
496 be redirected back to the original page.
497 """
498
499 def curry_wrapper(wrapped_function):
500 @wraps(wrapped_function)
501 def required_wrapper(*args, **kwargs):
502 return_url = decorator_kwargs.pop('return_url', request.url)
503
504 requested_scopes = set(self.scopes)
505 if scopes is not None:
506 requested_scopes |= set(scopes)
507 if self.has_credentials():
508 requested_scopes |= self.credentials.scopes
509
510 requested_scopes = list(requested_scopes)
511
512 # Does the user have credentials and does the credentials have
513 # all of the needed scopes?
514 if (self.has_credentials() and
515 self.credentials.has_scopes(requested_scopes)):
516 return wrapped_function(*args, **kwargs)
517 # Otherwise, redirect to authorization
518 else:
519 auth_url = self.authorize_url(
520 return_url,
521 scopes=requested_scopes,
522 **decorator_kwargs)
523
524 return redirect(auth_url)
525
526 return required_wrapper
527
528 if decorated_function:
529 return curry_wrapper(decorated_function)
530 else:
531 return curry_wrapper
532
533 def http(self, *args, **kwargs):
534 """Returns an authorized http instance.
535
536 Can only be called if there are valid credentials for the user, such
537 as inside of a view that is decorated with @required.
538
539 Args:
540 *args: Positional arguments passed to httplib2.Http constructor.
541 **kwargs: Positional arguments passed to httplib2.Http constructor.
542
543 Raises:
544 ValueError if no credentials are available.
545 """
546 if not self.credentials:
547 raise ValueError('No credentials available.')
548 return self.credentials.authorize(httplib2.Http(*args, **kwargs))
549
550
551 class FlaskSessionStorage(Storage):
552 """Storage implementation that uses Flask sessions.
553
554 Note that flask's default sessions are signed but not encrypted. Users
555 can see their own credentials and non-https connections can intercept user
556 credentials. We strongly recommend using a server-side session
557 implementation.
558 """
559
560 def locked_get(self):
561 serialized = session.get(_CREDENTIALS_KEY)
562
563 if serialized is None:
564 return None
565
566 credentials = OAuth2Credentials.from_json(serialized)
567 credentials.set_store(self)
568
569 return credentials
570
571 def locked_put(self, credentials):
572 session[_CREDENTIALS_KEY] = credentials.to_json()
573
574 def locked_delete(self):
575 if _CREDENTIALS_KEY in session:
576 del session[_CREDENTIALS_KEY]
OLDNEW
« no previous file with comments | « third_party/google-endpoints/oauth2client/file.py ('k') | third_party/google-endpoints/oauth2client/gce.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698