diff --git a/examples/openidexample/openidexample.py b/examples/openidexample/openidexample.py new file mode 100644 index 00000000..6a7301ea --- /dev/null +++ b/examples/openidexample/openidexample.py @@ -0,0 +1,165 @@ +# -*- coding: utf-8 -*- +""" + OpenID Example + ~~~~~~~~~~~~~~ + + This simple application shows how OpenID can be used in an application. + + Dependencies: + + - python-openid + - SQLAlchemy + + :copyright: (c) 2010 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +from flask import Flask, render_template, request, g, session, flash, \ + redirect, url_for, abort +from simpleopenid import SimpleOpenID + +from sqlalchemy import create_engine, Column, Integer, String +from sqlalchemy.orm import scoped_session, sessionmaker +from sqlalchemy.ext.declarative import declarative_base + +# configuration +DATABASE_URI = 'sqlite:////tmp/openidexample.db' +OPENID_FS_PATH = '/tmp/openidexample-store' +SECRET_KEY = 'development key' +DEBUG = True + +# setup flask +app = Flask(__name__) +app.debug = DEBUG +app.secret_key = SECRET_KEY + +# setup simpleopenid +oid = SimpleOpenID(OPENID_FS_PATH) + +# setup sqlalchemy +engine = create_engine(DATABASE_URI) +db_session = scoped_session(sessionmaker(autocommit=False, + autoflush=False, + bind=engine)) +Base = declarative_base() +Base.query = db_session.query_property() + +def init_db(): + Base.metadata.create_all(bind=engine) + + +class User(Base): + __tablename__ = 'users' + id = Column(Integer, primary_key=True) + name = Column(String(60)) + email = Column(String(200)) + openid = Column(String(200)) + + def __init__(self, name, email, openid): + self.name = name + self.email = email + self.openid = openid + + +@app.before_request +def before_request(): + g.user = None + if 'openid' in session: + g.user = User.query.filter_by(openid=session['openid']).first() + + +@app.route('/') +def index(): + return render_template('index.html') + + +@app.route('/login', methods=['GET', 'POST']) +@oid.loginhandler +def login(): + """Does the login via OpenID. Has to call into `oid.try_login` + to start the OpenID machinery. + """ + # if we are already logged in, go back to were we came from + if g.user is not None: + return redirect(oid.get_next_url()) + if request.method == 'POST': + openid = request.form.get('openid') + if openid: + return oid.try_login(openid) + return render_template('login.html', next_url=oid.get_next_url()) + + +@oid.after_login +def create_or_login(identity_url): + """This is called when login with OpenID succeeded and it's not + necessary to figure out if this is the users's first login or not. + This function has to redirect otherwise the user will be presented + with a terrible URL which we certainly don't want. + """ + session['openid'] = identity_url + user = User.query.filter_by(openid=identity_url).first() + if user is not None: + flash(u'Successfully signed in') + g.user = user + return redirect(oid.get_next_url()) + return redirect(url_for('create_profile', next=oid.get_next_url())) + + +@app.route('/create-profile', methods=['GET', 'POST']) +def create_profile(): + """If this is the user's first login, the create_or_login function + will redirect here so that the user can set up his profile. + """ + if g.user is not None or 'openid' not in session: + return redirect(url_for('index')) + if request.method == 'POST': + name = request.form['name'] + email = request.form['email'] + if not name: + flash(u'Error: you have to provide a name') + elif '@' not in email: + flash(u'Error: you have to enter a valid email address') + else: + flash(u'Profile successfully created') + db_session.add(User(name, email, session['openid'])) + db_session.commit() + return redirect(oid.get_next_url()) + return render_template('create_profile.html', next_url=oid.get_next_url()) + + +@app.route('/profile', methods=['GET', 'POST']) +def edit_profile(): + """Updates a profile""" + if g.user is None: + abort(401) + form = dict(name=g.user.name, email=g.user.email) + if request.method == 'POST': + if 'delete' in request.form: + db_session.delete(g.user) + db_session.commit() + session['openid'] = None + flash(u'Profile deleted') + return redirect(url_for('index')) + form['name'] = request.form['name'] + form['email'] = request.form['email'] + if not form['name']: + flash(u'Error: you have to provide a name') + elif '@' not in form['email']: + flash(u'Error: you have to enter a valid email address') + else: + flash(u'Profile successfully created') + g.user.name = form['name'] + g.user.email = form['email'] + db_session.commit() + return redirect(url_for('edit_profile')) + return render_template('edit_profile.html', form=form) + + +@app.route('/logout') +def logout(): + session.pop('openid', None) + flash(u'You were signed out') + return redirect(oid.get_next_url()) + + +if __name__ == '__main__': + app.run() diff --git a/examples/openidexample/simpleopenid.py b/examples/openidexample/simpleopenid.py new file mode 100644 index 00000000..c8e86463 --- /dev/null +++ b/examples/openidexample/simpleopenid.py @@ -0,0 +1,108 @@ +# -*- coding: utf-8 -*- +""" + simpleopenid + ~~~~~~~~~~~~ + + Tiny wrapper around python-openid to make working with the basic + API in a flask application easier. Adapt this code for your own + project if necessary. + + :copyright: (c) 2010 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +from functools import wraps + +from flask import request, session, flash, redirect +from werkzeug import url_quote + +from openid.association import Association +from openid.store.interface import OpenIDStore +from openid.store.filestore import FileOpenIDStore +from openid.consumer.consumer import Consumer, SUCCESS, CANCEL +from openid.consumer import discover +from openid.store import nonce + +# python-openid is a really stupid library in that regard, we have +# to disable logging by monkey patching +from openid import oidutil +oidutil.log = lambda *a, **kw: None + + +class SimpleOpenID(object): + """Simple helper class for OpenID auth.""" + + def __init__(self, store_path): + self.store_path = store_path + self.after_login_func = None + + def create_store(self): + """Creates the filesystem store""" + return FileOpenIDStore(self.store_path) + + def signal_error(self, msg): + """Signals an error. It does this by flashing a message""" + flash(u'Error: ' + msg) + + def get_next_url(self): + """Return the URL where we want to redirect to.""" + return request.values.get('next') or \ + request.referrer or \ + request.url_root + + def get_current_url(self): + """the current URL + next""" + return request.base_url + '?next=' + url_quote(self.get_next_url()) + + def get_success_url(self): + """Return the success URL""" + return self.get_current_url() + '&openid_complete=yes' + + def errorhandler(f): + """Called if an error occours with the message. By default + ``'Error: message'`` is flashed. + """ + self.signal_error = f + return f + + def after_login(self, f): + """This function will be called after login. It must redirect to + a different place and remember the user somewhere. The session + is not modified by SimpleOpenID. + """ + self.after_login_func = f + return f + + def loginhandler(self, f): + """Marks a function as login handler. This decorator injects some + more OpenID required logic. + """ + self.login_endpoint = f.__name__ + @wraps(f) + def decorated(*args, **kwargs): + if request.args.get('openid_complete') != u'yes': + return f(*args, **kwargs) + consumer = Consumer(session, self.create_store()) + openid_response = consumer.complete(request.args.to_dict(), + self.get_current_url()) + if openid_response.status == SUCCESS: + return self.after_login_func(openid_response.identity_url) + elif openid_response.status == CANCEL: + self.signal_error(u'The request was cancelled') + return redirect(self.get_current_url()) + self.signal_error(u'OpenID authentication error') + return redirect(self.get_current_url()) + return decorated + + def try_login(self, identity_url): + """This tries to login with the given identity URL. This function + must be called from the login_handler. + """ + try: + consumer = Consumer(session, self.create_store()) + auth_request = consumer.begin(identity_url) + except discover.DiscoveryFailure: + self.signal_error(u'The OpenID was invalid') + return redirect(self.get_current_url()) + trust_root = request.host_url + return redirect(auth_request.redirectURL(request.host_url, + self.get_success_url())) diff --git a/examples/openidexample/static/openid.png b/examples/openidexample/static/openid.png new file mode 100644 index 00000000..ce7954ab Binary files /dev/null and b/examples/openidexample/static/openid.png differ diff --git a/examples/openidexample/static/style.css b/examples/openidexample/static/style.css new file mode 100644 index 00000000..33d9816b --- /dev/null +++ b/examples/openidexample/static/style.css @@ -0,0 +1,39 @@ +body { + font-family: 'Georgia', serif; + font-size: 16px; + margin: 30px; + padding: 0; +} + +a { + color: #335E79; +} + +p.message { + color: #335E79; + padding: 10px; + background: #CADEEB; +} + +input { + font-family: 'Georgia', serif; + font-size: 16px; + border: 1px solid black; + color: #335E79; + padding: 2px; +} + +input[type="submit"] { + background: #CADEEB; + color: #335E79; + border-color: #335E79; +} + +input[name="openid"] { + background: url(openid.png) 4px no-repeat; + padding-left: 24px; +} + +h1, h2 { + font-weight: normal; +} diff --git a/examples/openidexample/templates/create_profile.html b/examples/openidexample/templates/create_profile.html new file mode 100644 index 00000000..73a469a2 --- /dev/null +++ b/examples/openidexample/templates/create_profile.html @@ -0,0 +1,22 @@ +{% extends "layout.html" %} +{% block title %}Create Profile{% endblock %} +{% block body %} +
+ Hey! This is the first time you signed in on this website. In + order to proceed we need a couple of more information from you: +
++ If you don't want to proceed, you can sign out again. +{% endblock %} diff --git a/examples/openidexample/templates/edit_profile.html b/examples/openidexample/templates/edit_profile.html new file mode 100644 index 00000000..a9b6b877 --- /dev/null +++ b/examples/openidexample/templates/edit_profile.html @@ -0,0 +1,16 @@ +{% extends "layout.html" %} +{% block title %}Edit Profile{% endblock %} +{% block body %} +
+ Hello {{ g.user.name }}! + {% endif %} +
+ This is just an example page so that something is here. +{% endblock %} diff --git a/examples/openidexample/templates/layout.html b/examples/openidexample/templates/layout.html new file mode 100644 index 00000000..3fad7c07 --- /dev/null +++ b/examples/openidexample/templates/layout.html @@ -0,0 +1,18 @@ + +