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 17Provides a Flask extension that makes using OAuth2 web server flow easier. 18The extension includes views that handle the entire auth flow and a 19``@required`` decorator to automatically ensure that user credentials are 20available. 21 22 23Configuration 24============= 25 26To configure, you'll need a set of OAuth2 web application credentials from the 27`Google Developer's Console <https://console.developers.google.com/project/_/\ 28apiui/credential>`__. 29 30.. code-block:: python 31 32 from oauth2client.contrib.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_FILE'] = '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 47Usage 48===== 49 50Once configured, you can use the :meth:`UserOAuth2.required` decorator to 51ensure 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 67If you want credentials to be optional for a view, you can leave the decorator 68off 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 81When 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 84available. 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 95URLs & Trigging Authorization 96============================= 97 98The extension will add two new routes to your application: 99 100 * ``"oauth2.authorize"`` -> ``/oauth2authorize`` 101 * ``"oauth2.callback"`` -> ``/oauth2callback`` 102 103When configuring your OAuth2 credentials on the Google Developer's Console, be 104sure to add ``http[s]://[your-app-url]/oauth2callback`` as an authorized 105callback url. 106 107Typically you don't not need to use these routes directly, just be sure to 108decorate any views that require credentials with ``@oauth2.required``. If 109needed, you can trigger authorization at any time by redirecting the user 110to 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 120Incremental Auth 121================ 122 123This extension also supports `Incremental Auth <https://developers.google.com\ 124/identity/protocols/OAuth2WebServer?hl=en#incrementalAuth>`__. To enable it, 125configure the extension with ``include_granted_scopes``. 126 127.. code-block:: python 128 129 oauth2 = UserOAuth2(app, include_granted_scopes=True) 130 131Then 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 146The decorator will ensure that the the user has authorized all specified scopes 147before allowing them to access the view, and will also ensure that credentials 148do not lose any previously authorized scopes. 149 150 151Storage 152======= 153 154By default, the extension uses a Flask session-based storage solution. This 155means that credentials are only available for the duration of a session. It 156also means that with Flask's default configuration, the credentials will be 157visible in the session cookie. It's highly recommended to use database-backed 158session and to use https whenever handling user credentials. 159 160If you need the credentials to be available longer than a user session or 161available outside of a request context, you will need to implement your own 162:class:`oauth2client.Storage`. 163""" 164 165from functools import wraps 166import hashlib 167import json 168import os 169import pickle 170 171try: 172 from flask import Blueprint 173 from flask import _app_ctx_stack 174 from flask import current_app 175 from flask import redirect 176 from flask import request 177 from flask import session 178 from flask import url_for 179 import markupsafe 180except ImportError: # pragma: NO COVER 181 raise ImportError('The flask utilities require flask 0.9 or newer.') 182 183import six.moves.http_client as httplib 184 185from oauth2client import client 186from oauth2client import clientsecrets 187from oauth2client import transport 188from oauth2client.contrib import dictionary_storage 189 190 191_DEFAULT_SCOPES = ('email',) 192_CREDENTIALS_KEY = 'google_oauth2_credentials' 193_FLOW_KEY = 'google_oauth2_flow_{0}' 194_CSRF_KEY = 'google_oauth2_csrf_token' 195 196 197def _get_flow_for_token(csrf_token): 198 """Retrieves the flow instance associated with a given CSRF token from 199 the Flask session.""" 200 flow_pickle = session.pop( 201 _FLOW_KEY.format(csrf_token), None) 202 203 if flow_pickle is None: 204 return None 205 else: 206 return pickle.loads(flow_pickle) 207 208 209class UserOAuth2(object): 210 """Flask extension for making OAuth 2.0 easier. 211 212 Configuration values: 213 214 * ``GOOGLE_OAUTH2_CLIENT_SECRETS_FILE`` path to a client secrets json 215 file, obtained from the credentials screen in the Google Developers 216 console. 217 * ``GOOGLE_OAUTH2_CLIENT_ID`` the oauth2 credentials' client ID. This 218 is only needed if ``GOOGLE_OAUTH2_CLIENT_SECRETS_FILE`` is not 219 specified. 220 * ``GOOGLE_OAUTH2_CLIENT_SECRET`` the oauth2 credentials' client 221 secret. This is only needed if ``GOOGLE_OAUTH2_CLIENT_SECRETS_FILE`` 222 is not specified. 223 224 If app is specified, all arguments will be passed along to init_app. 225 226 If no app is specified, then you should call init_app in your application 227 factory to finish initialization. 228 """ 229 230 def __init__(self, app=None, *args, **kwargs): 231 self.app = app 232 if app is not None: 233 self.init_app(app, *args, **kwargs) 234 235 def init_app(self, app, scopes=None, client_secrets_file=None, 236 client_id=None, client_secret=None, authorize_callback=None, 237 storage=None, **kwargs): 238 """Initialize this extension for the given app. 239 240 Arguments: 241 app: A Flask application. 242 scopes: Optional list of scopes to authorize. 243 client_secrets_file: Path to a file containing client secrets. You 244 can also specify the GOOGLE_OAUTH2_CLIENT_SECRETS_FILE config 245 value. 246 client_id: If not specifying a client secrets file, specify the 247 OAuth2 client id. You can also specify the 248 GOOGLE_OAUTH2_CLIENT_ID config value. You must also provide a 249 client secret. 250 client_secret: The OAuth2 client secret. You can also specify the 251 GOOGLE_OAUTH2_CLIENT_SECRET config value. 252 authorize_callback: A function that is executed after successful 253 user authorization. 254 storage: A oauth2client.client.Storage subclass for storing the 255 credentials. By default, this is a Flask session based storage. 256 kwargs: Any additional args are passed along to the Flow 257 constructor. 258 """ 259 self.app = app 260 self.authorize_callback = authorize_callback 261 self.flow_kwargs = kwargs 262 263 if storage is None: 264 storage = dictionary_storage.DictionaryStorage( 265 session, key=_CREDENTIALS_KEY) 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 ' 310 'the 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 = client.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 reason = markupsafe.escape(reason) 393 return ('Authorization failed: {0}'.format(reason), 394 httplib.BAD_REQUEST) 395 396 try: 397 encoded_state = request.args['state'] 398 server_csrf = session[_CSRF_KEY] 399 code = request.args['code'] 400 except KeyError: 401 return 'Invalid request', httplib.BAD_REQUEST 402 403 try: 404 state = json.loads(encoded_state) 405 client_csrf = state['csrf_token'] 406 return_url = state['return_url'] 407 except (ValueError, KeyError): 408 return 'Invalid request state', httplib.BAD_REQUEST 409 410 if client_csrf != server_csrf: 411 return 'Invalid request state', httplib.BAD_REQUEST 412 413 flow = _get_flow_for_token(server_csrf) 414 415 if flow is None: 416 return 'Invalid request state', httplib.BAD_REQUEST 417 418 # Exchange the auth code for credentials. 419 try: 420 credentials = flow.step2_exchange(code) 421 except client.FlowExchangeError as exchange_error: 422 current_app.logger.exception(exchange_error) 423 content = 'An error occurred: {0}'.format(exchange_error) 424 return content, httplib.BAD_REQUEST 425 426 # Save the credentials to the storage. 427 self.storage.put(credentials) 428 429 if self.authorize_callback: 430 self.authorize_callback(credentials) 431 432 return redirect(return_url) 433 434 @property 435 def credentials(self): 436 """The credentials for the current user or None if unavailable.""" 437 ctx = _app_ctx_stack.top 438 439 if not hasattr(ctx, _CREDENTIALS_KEY): 440 ctx.google_oauth2_credentials = self.storage.get() 441 442 return ctx.google_oauth2_credentials 443 444 def has_credentials(self): 445 """Returns True if there are valid credentials for the current user.""" 446 if not self.credentials: 447 return False 448 # Is the access token expired? If so, do we have an refresh token? 449 elif (self.credentials.access_token_expired and 450 not self.credentials.refresh_token): 451 return False 452 else: 453 return True 454 455 @property 456 def email(self): 457 """Returns the user's email address or None if there are no credentials. 458 459 The email address is provided by the current credentials' id_token. 460 This should not be used as unique identifier as the user can change 461 their email. If you need a unique identifier, use user_id. 462 """ 463 if not self.credentials: 464 return None 465 try: 466 return self.credentials.id_token['email'] 467 except KeyError: 468 current_app.logger.error( 469 'Invalid id_token {0}'.format(self.credentials.id_token)) 470 471 @property 472 def user_id(self): 473 """Returns the a unique identifier for the user 474 475 Returns None if there are no credentials. 476 477 The id is provided by the current credentials' id_token. 478 """ 479 if not self.credentials: 480 return None 481 try: 482 return self.credentials.id_token['sub'] 483 except KeyError: 484 current_app.logger.error( 485 'Invalid id_token {0}'.format(self.credentials.id_token)) 486 487 def authorize_url(self, return_url, **kwargs): 488 """Creates a URL that can be used to start the authorization flow. 489 490 When the user is directed to the URL, the authorization flow will 491 begin. Once complete, the user will be redirected to the specified 492 return URL. 493 494 Any kwargs are passed into the flow constructor. 495 """ 496 return url_for('oauth2.authorize', return_url=return_url, **kwargs) 497 498 def required(self, decorated_function=None, scopes=None, 499 **decorator_kwargs): 500 """Decorator to require OAuth2 credentials for a view. 501 502 If credentials are not available for the current user, then they will 503 be redirected to the authorization flow. Once complete, the user will 504 be redirected back to the original page. 505 """ 506 507 def curry_wrapper(wrapped_function): 508 @wraps(wrapped_function) 509 def required_wrapper(*args, **kwargs): 510 return_url = decorator_kwargs.pop('return_url', request.url) 511 512 requested_scopes = set(self.scopes) 513 if scopes is not None: 514 requested_scopes |= set(scopes) 515 if self.has_credentials(): 516 requested_scopes |= self.credentials.scopes 517 518 requested_scopes = list(requested_scopes) 519 520 # Does the user have credentials and does the credentials have 521 # all of the needed scopes? 522 if (self.has_credentials() and 523 self.credentials.has_scopes(requested_scopes)): 524 return wrapped_function(*args, **kwargs) 525 # Otherwise, redirect to authorization 526 else: 527 auth_url = self.authorize_url( 528 return_url, 529 scopes=requested_scopes, 530 **decorator_kwargs) 531 532 return redirect(auth_url) 533 534 return required_wrapper 535 536 if decorated_function: 537 return curry_wrapper(decorated_function) 538 else: 539 return curry_wrapper 540 541 def http(self, *args, **kwargs): 542 """Returns an authorized http instance. 543 544 Can only be called if there are valid credentials for the user, such 545 as inside of a view that is decorated with @required. 546 547 Args: 548 *args: Positional arguments passed to httplib2.Http constructor. 549 **kwargs: Positional arguments passed to httplib2.Http constructor. 550 551 Raises: 552 ValueError if no credentials are available. 553 """ 554 if not self.credentials: 555 raise ValueError('No credentials available.') 556 return self.credentials.authorize( 557 transport.get_http_object(*args, **kwargs)) 558