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 179except ImportError: # pragma: NO COVER 180 raise ImportError('The flask utilities require flask 0.9 or newer.') 181 182import httplib2 183import six.moves.http_client as httplib 184 185from oauth2client import client 186from oauth2client import clientsecrets 187from oauth2client.contrib import dictionary_storage 188 189 190__author__ = 'jonwayne@google.com (Jon Wayne Parrott)' 191 192_DEFAULT_SCOPES = ('email',) 193_CREDENTIALS_KEY = 'google_oauth2_credentials' 194_FLOW_KEY = 'google_oauth2_flow_{0}' 195_CSRF_KEY = 'google_oauth2_csrf_token' 196 197 198def _get_flow_for_token(csrf_token): 199 """Retrieves the flow instance associated with a given CSRF token from 200 the Flask session.""" 201 flow_pickle = session.pop( 202 _FLOW_KEY.format(csrf_token), None) 203 204 if flow_pickle is None: 205 return None 206 else: 207 return pickle.loads(flow_pickle) 208 209 210class UserOAuth2(object): 211 """Flask extension for making OAuth 2.0 easier. 212 213 Configuration values: 214 215 * ``GOOGLE_OAUTH2_CLIENT_SECRETS_FILE`` path to a client secrets json 216 file, obtained from the credentials screen in the Google Developers 217 console. 218 * ``GOOGLE_OAUTH2_CLIENT_ID`` the oauth2 credentials' client ID. This 219 is only needed if ``GOOGLE_OAUTH2_CLIENT_SECRETS_FILE`` is not 220 specified. 221 * ``GOOGLE_OAUTH2_CLIENT_SECRET`` the oauth2 credentials' client 222 secret. This is only needed if ``GOOGLE_OAUTH2_CLIENT_SECRETS_FILE`` 223 is not specified. 224 225 If app is specified, all arguments will be passed along to init_app. 226 227 If no app is specified, then you should call init_app in your application 228 factory to finish initialization. 229 """ 230 231 def __init__(self, app=None, *args, **kwargs): 232 self.app = app 233 if app is not None: 234 self.init_app(app, *args, **kwargs) 235 236 def init_app(self, app, scopes=None, client_secrets_file=None, 237 client_id=None, client_secret=None, authorize_callback=None, 238 storage=None, **kwargs): 239 """Initialize this extension for the given app. 240 241 Arguments: 242 app: A Flask application. 243 scopes: Optional list of scopes to authorize. 244 client_secrets_file: Path to a file containing client secrets. You 245 can also specify the GOOGLE_OAUTH2_CLIENT_SECRETS_FILE config 246 value. 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 = dictionary_storage.DictionaryStorage( 266 session, key=_CREDENTIALS_KEY) 267 self.storage = storage 268 269 if scopes is None: 270 scopes = app.config.get('GOOGLE_OAUTH2_SCOPES', _DEFAULT_SCOPES) 271 self.scopes = scopes 272 273 self._load_config(client_secrets_file, client_id, client_secret) 274 275 app.register_blueprint(self._create_blueprint()) 276 277 def _load_config(self, client_secrets_file, client_id, client_secret): 278 """Loads oauth2 configuration in order of priority. 279 280 Priority: 281 1. Config passed to the constructor or init_app. 282 2. Config passed via the GOOGLE_OAUTH2_CLIENT_SECRETS_FILE app 283 config. 284 3. Config passed via the GOOGLE_OAUTH2_CLIENT_ID and 285 GOOGLE_OAUTH2_CLIENT_SECRET app config. 286 287 Raises: 288 ValueError if no config could be found. 289 """ 290 if client_id and client_secret: 291 self.client_id, self.client_secret = client_id, client_secret 292 return 293 294 if client_secrets_file: 295 self._load_client_secrets(client_secrets_file) 296 return 297 298 if 'GOOGLE_OAUTH2_CLIENT_SECRETS_FILE' in self.app.config: 299 self._load_client_secrets( 300 self.app.config['GOOGLE_OAUTH2_CLIENT_SECRETS_FILE']) 301 return 302 303 try: 304 self.client_id, self.client_secret = ( 305 self.app.config['GOOGLE_OAUTH2_CLIENT_ID'], 306 self.app.config['GOOGLE_OAUTH2_CLIENT_SECRET']) 307 except KeyError: 308 raise ValueError( 309 'OAuth2 configuration could not be found. Either specify the ' 310 'client_secrets_file or client_id and client_secret or set ' 311 'the app configuration variables ' 312 'GOOGLE_OAUTH2_CLIENT_SECRETS_FILE or ' 313 'GOOGLE_OAUTH2_CLIENT_ID and GOOGLE_OAUTH2_CLIENT_SECRET.') 314 315 def _load_client_secrets(self, filename): 316 """Loads client secrets from the given filename.""" 317 client_type, client_info = clientsecrets.loadfile(filename) 318 if client_type != clientsecrets.TYPE_WEB: 319 raise ValueError( 320 'The flow specified in {0} is not supported.'.format( 321 client_type)) 322 323 self.client_id = client_info['client_id'] 324 self.client_secret = client_info['client_secret'] 325 326 def _make_flow(self, return_url=None, **kwargs): 327 """Creates a Web Server Flow""" 328 # Generate a CSRF token to prevent malicious requests. 329 csrf_token = hashlib.sha256(os.urandom(1024)).hexdigest() 330 331 session[_CSRF_KEY] = csrf_token 332 333 state = json.dumps({ 334 'csrf_token': csrf_token, 335 'return_url': return_url 336 }) 337 338 kw = self.flow_kwargs.copy() 339 kw.update(kwargs) 340 341 extra_scopes = kw.pop('scopes', []) 342 scopes = set(self.scopes).union(set(extra_scopes)) 343 344 flow = client.OAuth2WebServerFlow( 345 client_id=self.client_id, 346 client_secret=self.client_secret, 347 scope=scopes, 348 state=state, 349 redirect_uri=url_for('oauth2.callback', _external=True), 350 **kw) 351 352 flow_key = _FLOW_KEY.format(csrf_token) 353 session[flow_key] = pickle.dumps(flow) 354 355 return flow 356 357 def _create_blueprint(self): 358 bp = Blueprint('oauth2', __name__) 359 bp.add_url_rule('/oauth2authorize', 'authorize', self.authorize_view) 360 bp.add_url_rule('/oauth2callback', 'callback', self.callback_view) 361 362 return bp 363 364 def authorize_view(self): 365 """Flask view that starts the authorization flow. 366 367 Starts flow by redirecting the user to the OAuth2 provider. 368 """ 369 args = request.args.to_dict() 370 371 # Scopes will be passed as mutliple args, and to_dict() will only 372 # return one. So, we use getlist() to get all of the scopes. 373 args['scopes'] = request.args.getlist('scopes') 374 375 return_url = args.pop('return_url', None) 376 if return_url is None: 377 return_url = request.referrer or '/' 378 379 flow = self._make_flow(return_url=return_url, **args) 380 auth_url = flow.step1_get_authorize_url() 381 382 return redirect(auth_url) 383 384 def callback_view(self): 385 """Flask view that handles the user's return from OAuth2 provider. 386 387 On return, exchanges the authorization code for credentials and stores 388 the credentials. 389 """ 390 if 'error' in request.args: 391 reason = request.args.get( 392 'error_description', request.args.get('error', '')) 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(httplib2.Http(*args, **kwargs)) 557