Files
deb-python-oauth2client/oauth2client/flask_util.py
Jon Wayne Parrott 5a0bbfd2c5 Flask 3LO helper
2015-07-30 15:52:32 -07:00

419 lines
15 KiB
Python

# Copyright 2015 Google Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Utilities for the Flask web framework
Provides a Flask extension that makes using OAuth2 web server flow easier.
The extension includes views that handle the entire auth flow and a @required
decorator to automatically ensure that user credentials are available.
To configure::
from oauth2client.flask_util import UserOAuth2
app = Flask(__name__)
app.config['SECRET_KEY'] = 'your-secret-key'
app.config['OAUTH2_CLIENT_SECRETS_JSON'] = 'client_secrets.json'
# or, specify the client id and secret separately
app.config['OAUTH2_CLIENT_ID'] = 'your-client-id'
app.config['OAUTH2_CLIENT_SECRET'] = 'your-client-secret'
oauth2 = UserOAuth2(app)
To use::
# Note that app.route should be the outermost decorator.
@app.route('/needs_credentials')
@oauth2.required
def example():
# http is authorized with the user's credentials and can be used
# to make http calls.
http = oauth2.http()
# Or, you can access the credentials directly
credentials = oauth2.credentials
@app.route('/info')
@oauth2.required
def info():
return "Hello, {}".format(oauth2.email)
@app.route('/optional')
def optional():
if oauth2.has_credentials():
return 'Credentials found!'
else:
return 'No credentials!'
"""
__author__ = 'jonwayne@google.com (Jon Wayne Parrott)'
import hashlib
import json
import os
from functools import wraps
import six.moves.http_client as httplib
import httplib2
try:
from flask import Blueprint
from flask import _app_ctx_stack
from flask import current_app
from flask import redirect
from flask import request
from flask import session
from flask import url_for
except ImportError:
raise ImportError('The flask utilities require flask 0.9 or newer.')
from oauth2client.client import FlowExchangeError
from oauth2client.client import OAuth2Credentials
from oauth2client.client import OAuth2WebServerFlow
from oauth2client.client import Storage
from oauth2client import clientsecrets
from oauth2client import util
DEFAULT_SCOPES = ('email',)
class UserOAuth2(object):
"""Flask extension for making OAuth 2.0 easier.
Configuration values:
* GOOGLE_OAUTH2_CLIENT_SECRETS_JSON path to a client secrets json file,
obtained from the credentials screen in the Google Developers
console.
* GOOGLE_OAUTH2_CLIENT_ID the oauth2 credentials' client ID. This is
only needed if OAUTH2_CLIENT_SECRETS_JSON is not specified.
* GOOGLE_OAUTH2_CLIENT_SECRET the oauth2 credentials' client secret.
This is only needed if OAUTH2_CLIENT_SECRETS_JSON is not specified.
If app is specified, all arguments will be passed along to init_app.
If no app is specified, then you should call init_app in your application
factory to finish initialization.
"""
def __init__(self, app=None, *args, **kwargs):
self.app = app
if app is not None:
self.init_app(app, *args, **kwargs)
def init_app(self, app, scopes=None, client_secrets_file=None,
client_id=None, client_secret=None, authorize_callback=None,
storage=None, **kwargs):
"""Initialize this extension for the given app.
Arguments:
app: A Flask application.
scopes: Optional list of scopes to authorize.
client_secrets_file: Path to a file containing client secrets. You
can also specify the OAUTH2_CLIENT_SECRETS_JSON config value.
client_id: If not specifying a client secrets file, specify the
OAuth2 client id. You can also specify the
GOOGLE_OAUTH2_CLIENT_ID config value. You must also provide a
client secret.
client_secret: The OAuth2 client secret. You can also specify the
GOOGLE_OAUTH2_CLIENT_SECRET config value.
authorize_callback: A function that is executed after successful
user authorization.
storage: A oauth2client.client.Storage subclass for storing the
credentials. By default, this is a Flask session based storage.
kwargs: Any additional args are passed along to the Flow
constructor.
"""
self.app = app
self.authorize_callback = authorize_callback
self.flow_kwargs = kwargs
if storage is None:
storage = FlaskSessionStorage()
self.storage = storage
if scopes is None:
scopes = app.config.get('GOOGLE_OAUTH2_SCOPES', DEFAULT_SCOPES)
self.scopes = scopes
self._load_config(client_secrets_file, client_id, client_secret)
app.register_blueprint(self._create_blueprint())
def _load_config(self, client_secrets_file, client_id, client_secret):
"""Loads oauth2 configuration in order of priority.
Priority:
1. Config passed to the constructor or init_app.
2. Config passed via the GOOGLE_OAUTH2_CLIENT_SECRETS_FILE app
config.
3. Config passed via the GOOGLE_OAUTH2_CLIENT_ID and
GOOGLE_OAUTH2_CLIENT_SECRET app config.
Raises:
ValueError if no config could be found.
"""
if client_id and client_secret:
self.client_id, self.client_secret = client_id, client_secret
return
if client_secrets_file:
self._load_client_secrets(client_secrets_file)
return
if 'GOOGLE_OAUTH2_CLIENT_SECRETS_FILE' in self.app.config:
self._load_client_secrets(
self.app.config['GOOGLE_OAUTH2_CLIENT_SECRETS_FILE'])
return
try:
self.client_id, self.client_secret = (
self.app.config['GOOGLE_OAUTH2_CLIENT_ID'],
self.app.config['GOOGLE_OAUTH2_CLIENT_SECRET'])
except KeyError:
raise ValueError(
'OAuth2 configuration could not be found. Either specify the '
'client_secrets_file or client_id and client_secret or set the'
'app configuration variables GOOGLE_OAUTH2_CLIENT_SECRETS_FILE '
'or GOOGLE_OAUTH2_CLIENT_ID and GOOGLE_OAUTH2_CLIENT_SECRET.')
def _load_client_secrets(self, filename):
"""Loads client secrets from the given filename."""
client_type, client_info = clientsecrets.loadfile(filename)
if client_type != clientsecrets.TYPE_WEB:
raise ValueError(
'The flow specified in %s is not supported.' % client_type)
self.client_id = client_info['client_id']
self.client_secret = client_info['client_secret']
def _make_flow(self, return_url=None, **kwargs):
"""Creates a Web Server Flow"""
# Generate a CSRF token to prevent malicious requests.
csrf_token = hashlib.sha256(os.urandom(1024)).hexdigest()
session['google_oauth2_csrf_token'] = csrf_token
state = json.dumps({
'csrf_token': csrf_token,
'return_url': return_url
})
kw = self.flow_kwargs.copy()
kw.update(kwargs)
extra_scopes = util.scopes_to_string(kw.pop('scopes', ''))
scopes = ' '.join([util.scopes_to_string(self.scopes), extra_scopes])
return OAuth2WebServerFlow(
client_id=self.client_id,
client_secret=self.client_secret,
scope=scopes,
state=state,
redirect_uri=url_for('oauth2.callback', _external=True),
**kw)
def _create_blueprint(self):
bp = Blueprint('oauth2', __name__)
bp.add_url_rule('/oauth2authorize', 'authorize', self.authorize_view)
bp.add_url_rule('/oauth2callback', 'callback', self.callback_view)
return bp
def authorize_view(self):
"""Flask view that starts the authorization flow by redirecting the
user to the OAuth2 provider."""
args = request.args.to_dict()
return_url = args.pop('return_url', None)
if return_url is None:
return_url = request.referrer or '/'
flow = self._make_flow(return_url=return_url, **args)
auth_url = flow.step1_get_authorize_url()
return redirect(auth_url)
def callback_view(self):
"""Flask view that handles the user's return from the OAuth2 provider
and exchanges the authorization code for credentials and stores the
credentials."""
if 'error' in request.args:
reason = request.args.get(
'error_description', request.args.get('error', ''))
return 'Authorization failed: %s' % reason, httplib.BAD_REQUEST
try:
encoded_state = request.args['state']
server_csrf = session['google_oauth2_csrf_token']
code = request.args['code']
except KeyError:
return 'Invalid request', httplib.BAD_REQUEST
try:
state = json.loads(encoded_state)
client_csrf = state['csrf_token']
return_url = state['return_url']
except (ValueError, KeyError):
return 'Invalid request state', httplib.BAD_REQUEST
if client_csrf != server_csrf:
return 'Invalid request state', httplib.BAD_REQUEST
flow = self._make_flow()
# Exchange the auth code for credentials.
try:
credentials = flow.step2_exchange(code)
except FlowExchangeError as exchange_error:
current_app.logger.exception(exchange_error)
return 'An error occurred: %s' % exchange_error, httplib.BAD_REQUEST
# Save the credentials to the storage.
self.storage.put(credentials)
if self.authorize_callback:
self.authorize_callback(credentials)
return redirect(return_url)
@property
def credentials(self):
"""The credentials for the current user or None if unavailable."""
ctx = _app_ctx_stack.top
if not hasattr(ctx, 'google_oauth2_credentials'):
ctx.google_oauth2_credentials = self.storage.get()
return ctx.google_oauth2_credentials
def has_credentials(self):
"""Returns True if there are valid credentials for the current user."""
return self.credentials and not self.credentials.invalid
@property
def email(self):
"""Returns the user's email address or None if there are no credentials.
The email address is provided by the current credentials' id_token. This
should not be used as unique identifier as the user can change their
email. If you need a unique identifier, use user_id.
"""
if not self.credentials:
return None
try:
return self.credentials.id_token['email']
except KeyError:
current_app.logger.error(
'Invalid id_token %s', self.credentials.id_token)
@property
def user_id(self):
"""Returns the a unique identifier for the user or None if there are no
credentials.
The id is provided by the current credentials' id_token.
"""
if not self.credentials:
return None
try:
return self.credentials.id_token['sub']
except KeyError:
current_app.logger.error(
'Invalid id_token %s', self.credentials.id_token)
def authorize_url(self, return_url, **kwargs):
"""Creates a URL that can be used to start the authorization flow.
When the user is directed to the URL, the authorization flow will begin.
Once complete, the user will be redirected to the specified return URL.
Any kwargs are passed into the flow constructor.
"""
return url_for('oauth2.authorize', return_url=return_url, **kwargs)
def required(self, decorated_function=None, **decorator_kwargs):
"""Decorator to require OAuth2 credentials for a view.
If credentials are not available for the current user, then they will
be redirected to the authorization flow. Once complete, the user will
be redirected back to the original page.
"""
def curry_wrapper(wrapped_function):
@wraps(wrapped_function)
def required_wrapper(*args, **kwargs):
if not self.has_credentials():
if 'return_url' not in decorator_kwargs:
decorator_kwargs['return_url'] = request.url
return redirect(self.authorize_url(**decorator_kwargs))
else:
return wrapped_function(*args, **kwargs)
return required_wrapper
if decorated_function:
return curry_wrapper(decorated_function)
else:
return curry_wrapper
def http(self, *args, **kwargs):
"""Returns an authorized http instance.
Can only be called if there are valid credentials for the user, such
as inside of a view that is decorated with @required.
Args:
*args: Positional arguments passed to httplib2.Http constructor.
**kwargs: Positional arguments passed to httplib2.Http constructor.
Raises:
ValueError if no credentials are available.
"""
if not self.credentials:
raise ValueError('No credentials available.')
return self.credentials.authorize(httplib2.Http(*args, **kwargs))
class FlaskSessionStorage(Storage):
"""Storage implementation that uses Flask sessions.
Note that flask's default sessions are signed but not encrypted. Users
can see their own credentials and non-https connections can intercept user
credentials. We strongly recommend using a server-side session
implementation.
"""
def locked_get(self):
serialized = session.get('google_oauth2_credentials')
if serialized is None:
return None
credentials = OAuth2Credentials.from_json(serialized)
if credentials:
credentials.set_store(self)
return credentials
def locked_put(self, credentials):
session['google_oauth2_credentials'] = credentials.to_json()
def locked_delete(self):
if 'google_oauth2_credentials' in session:
del session['google_oauth2_credentials']