Skip to content
This repository has been archived by the owner on Jan 18, 2025. It is now read-only.

Commit

Permalink
Flask 3LO helper
Browse files Browse the repository at this point in the history
  • Loading branch information
Jon Wayne Parrott committed Jul 23, 2015
1 parent c8c8987 commit 26112aa
Showing 1 changed file with 299 additions and 0 deletions.
299 changes: 299 additions & 0 deletions oauth2client/flask.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,299 @@
# Copyright 2015 Google Inc.
#
# 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
"""
from __future__ import absolute_import
import json
import random
import string
from functools import wraps

import httplib2
from flask import (Blueprint, current_app, redirect, request, session, url_for,
_app_ctx_stack)
from oauth2client.client import (Credentials, OAuth2WebServerFlow,
FlowExchangeError)
from oauth2client import clientsecrets


DEFAULT_SCOPES = ['email', 'profile']


class ConfigurationError(Exception):
"""Raised when OAuth2 settings are not adequately configured."""
pass


class NoCredentialsError(Exception):
"""Raised when attempting to use credentials that do not exist."""
pass


class OAuth2(object):
"""Flask extension for making OAuth 2.0 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::
app = Flask(__name__)
app.config['SECRET_KEY'] = 'your-secret-key'
app.config['OAUTH2_CLIENT_SECRETS_JSON'] = 'client_secrets.json'
oauth2 = OAuth2(app)
To use::
@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)
"""

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,
logout_callback=None):
self.app = app

if not scopes:
scopes = app.config.get('OAUTH2_SCOPES', DEFAULT_SCOPES)

self.scopes = scopes

self.client_id, self.client_secret = client_id, client_secret

if not self.client_id or not self.client_secret:
if client_secrets_file:
self.client_id, self.client_secret = self._load_client_secrets(
client_secrets_file)
elif 'OAUTH2_CLIENT_SECRETS_JSON' in app.config:
self.client_id, self.client_secret = self._load_client_secrets(
app.config['OAUTH2_CLIENT_SECRETS_JSON'])
else:
self.client_id, self.client_secret = self._load_app_config(app)

app.register_blueprint(self._create_blueprint())

self.authorize_callback = authorize_callback
self.logout_callback = logout_callback

app.after_request(self._update_session)

def _load_client_secrets(self, filename):
client_type, client_info = clientsecrets.loadfile(filename)
if client_type != clientsecrets.TYPE_WEB:
raise ConfigurationError(
"The flow specified in %s isn't supported." % client_type)

return client_info['client_id'], client_info['client_secret']

def _load_app_config(self, app):
if ('OAUTH2_CLIENT_SECRET' not in app.config or
'OAUTH2_CLIENT_ID' not in app.config):
raise ConfigurationError(
"OAuth2 configuration could not be found. "
"Either OAUTH2_CLIENT_SECRETS_JSON or OAUTH2_CLIENT_ID and "
"OAUTH2_CLIENT_SECRET must be specified.")

return (app.config['OAUTH2_CLIENT_ID'],
app.config['OAUTH2_CLIENT_SECRET'])

def _update_session(self, resp):
"""Ensures that the session's copy of the credentials is updated
when the credential refreshes its access token."""
ctx = _app_ctx_stack.top
if hasattr(ctx, 'oauth2_credentials'):
session['oauth2credentials'] = ctx.oauth2_credentials.to_json()
return resp

def _request_user_info(self):
"""
Makes an HTTP request to the Google+ API to retrieve the user's basic
profile information, including full name and photo.
"""
http = self.http()
resp, content = http.request(
'https://www.googleapis.com/plus/v1/people/me')

if resp.status != 200:
current_app.logger.error(
"Error while obtaining user profile: %s" % resp)
return None

profile = json.loads(content)
return profile

def _make_flow(self):
# Generate a CSRF token to prevent malicious requests.
state = ''.join(random.choice(string.ascii_uppercase + string.digits)
for x in range(32))

session['oauth2state'] = state

return OAuth2WebServerFlow(
client_id=self.client_id,
client_secret=self.client_secret,
scope=self.scopes, # TODO
state=state,
approval_prompt='force', # TODO
redirect_uri=url_for("oauth2.callback", _external=True))

def _create_blueprint(self):
bp = Blueprint('oauth2', __name__)
bp.add_url_rule('/oauth2authorize', 'authorize', self.authorize_view)
bp.add_url_rule('/oauth2logout', 'logout', self.logout_view)
bp.add_url_rule('/oauth2callback', 'callback', self.callback_view)

return bp

def authorize_view(self):
return_url = request.args.get('return_url')
if not return_url:
return_url = request.referrer or '/'
session['oauth2return'] = return_url

flow = self._make_flow()
auth_url = flow.step1_get_authorize_url()
return redirect(auth_url)

def logout_view(self):
session.pop('oauth2credentials', None)
session.pop('oauth2profile', None)
session.pop('oauth2userid', None)

if self.logout_callback:
self.logout_callback()

return redirect(request.referrer or '/')

def callback_view(self):
# Check the CSRF token.
if request.args.get('state', '') != session.pop('oauth2state', None):
return 'Invalid request state', 400

flow = self._make_flow()

# Exchange the auth code for credentials.
try:
credentials = flow.step2_exchange(request.args['code'])
except FlowExchangeError as e:
current_app.logger.exception(e)
return e.message, 400

# Save the credentials to the session.
session['oauth2credentials'] = credentials.to_json()

# Get the user's profile from Google and save it to the session.
if 'profile' in self.scopes:
session['oauth2profile'] = self._request_user_info()

if self.authorize_callback:
self.authorize_callback()

return redirect(session.pop('oauth2return', '/'))

@property
def credentials(self):
ctx = _app_ctx_stack.top

if not hasattr(ctx, 'oauth2_credentials'):
serialized = session.get('oauth2credentials')

if not serialized:
return None

ctx.oauth2_credentials = Credentials.new_from_json(serialized)

return ctx.oauth2_credentials

def has_credentials(self):
return self.credentials and not self.credentials.invalid

@property
def email(self):
"""Returns the user's email address.
The email address is provided by the current credentials' id_token. The
use can change their email, it's more appropriate to use user_id for
a unique, user-specific identifier.
"""
if not self.credentials:
return None
return self.credentials.id_token['email']

@property
def user_id(self):
"""Returns the user's email address.
The id is provided by the current credentials' id_token.
"""
if not self.credentials:
return None
return self.credentials.id_token['sub']

@property
def profile(self):
"""Return's the user's full Google profile.
This is only available if "profile" is specified in the scopes.
"""
return session.get('oauth2profile', None)

def required(self, f):
"""Decorator to require OAuth2 credentials for a view.
"""
@wraps(f)
def decr(*args, **kwargs):
if not self.has_credentials():
return redirect(
url_for('oauth2.authorize', return_url=request.url))
else:
return f(*args, **kwargs)
return decr

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.
"""
if not self.credentials:
raise NoCredentialsError()
return self.credentials.authorize(httplib2.Http(*args, **kwargs))

0 comments on commit 26112aa

Please sign in to comment.