Flask-OpenID

Flask-OpenID is an extension to Flask that allows you to add OpenID based authentication to your website in a matter of minutes. It depends on Flask and python-openid 2.x. You can install the requirements from PyPI with easy_install or pip or download them by hand.

Features

  • support for OpenID 2.x
  • friendly API
  • perfect integration into Flask
  • basic support for AX and SReg extensions to OpenID that make it possible to fetch basic profile information from a user’s OpenID provider.

Installation

Install the extension with one of the following commands:

$ easy_install Flask-OpenID

or alternatively if you have pip installed:

$ pip install Flask-OpenID

How to Use

To integrate Flask-OpenID into your application you need to create an instance of the OpenID object first:

from flask.ext.openid import OpenID
oid = OpenID(app, '/path/to/store', safe_roots=[])

By default it will use the filesystem as store for information needed by OpenID for the authentication process. You can alternatively implement your own store that uses the database or a no-sql server. For more information about that, consult the python-openid documentation.

The path to the store can also be specified with the OPENID_FS_STORE_PATH configuration variable.

Alternatively the object can be instantiated without the application in which case it can later be registered for an application with the init_app() method.

The list of URL roots that are safe to redirect the user to are passed via safe_roots. Whenever the url root of the 'next' request argument is not in this list, the user will get redirected to the app root. All urls that are local to the current app are always regared as trusted. This security mechanism can be disabled by leaving safe_roots out, but this is not suggested.

The current logged in user has to be memorized somewhere, we will use the 'openid' key in the session. This can be implemented in a before_request function:

from flask import g, session

@app.before_request
def lookup_current_user():
    g.user = None
    if 'openid' in session:
        openid = session['openid']
        g.user = User.query.filter_by(openid=openid).first()

This assumes the openid used for a user is stored in the user table itself. As you can see from the example above, we’re using SQLAlchemy here, but feel free to use a different storage backend. It’s just important that you can somehow map from openid URL to user.

Next you need to define a login handling function. This function is a standard view function that is additionally decorated as loginhandler():

@app.route('/login', methods=['GET', 'POST'])
@oid.loginhandler
def login():
    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, ask_for=['email', 'nickname'],
                                         ask_for_optional=['fullname'])
    return render_template('login.html', next=oid.get_next_url(),
                           error=oid.fetch_error())

What’s happening inside the login handler is that first we try to figure out if the user is already logged in. In that case we return to where we just came from (get_next_url() can do that for us). When the data is submitted we get the openid the user entered and try to login with that information. Additionally we ask the openid provider for email, nickname and the user’s full name, where we declare full name as optional. If that information is available, we can use it to simplify the account creation process in our application.

The template also needs the URL we want to return to, because it has to forward that information in the form. If an error happened, fetch_error() will return that error message for us.

This is what a login template typically looks like:

{% extends "layout.html" %}
{% block title %}Sign in{% endblock %}
{% block body %}
  <h2>Sign in</h2>
  <form action="" method=post>
    {% if error %}<p class=error><strong>Error:</strong> {{ error }}</p>{% endif %}
    <p>
      OpenID:
      <input type=text name=openid size=30>
      <input type=submit value="Sign in">
      <input type=hidden name=next value="{{ next }}">
  </form>
{% endblock %}

See how error and next are used. The name of the form field next is required, so don’t change it.

Responding to Successful Logins

Next we have to define a function that is called after the login was successful. The responsibility of that function is to remember the user that just logged in and to figure out if it’s a new user to the system or one with an existing profile (if you want to use profiles).

Such a function is decorated with after_login() and must remember the user in the session and redirect to the proper page:

from flask import flash

@oid.after_login
def create_or_login(resp):
    session['openid'] = resp.identity_url
    user = User.query.filter_by(openid=resp.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(),
                            name=resp.fullname or resp.nickname,
                            email=resp.email))

The resp object passed is a OpenIDResponse object with all the information you might desire. As you can see, we memorize the user’s openid and try to get the user with that OpenID from the database. If that fails we redirect the user to a page to create a new profile and also forward the name (or nickname if no name is provided) and the email address. Please keep in mind that an openid provider does not have to support these profile information and not every value you ask for will be there. If it’s missing it will be None. Again make sure to not lose the information about the next URL.

Creating a Profile

A typical page to create such a profile might look like this:

@app.route('/create-profile', methods=['GET', 'POST'])
def create_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=oid.get_next_url())

If you’re using the same names for the URL parameters in the step before and in this form, you have nice looking and simple templates:

{% extends "layout.html" %}
{% block title %}Create Profile{% endblock %}
{% block body %}
  <h2>Create Profile</h2>
  <p>
    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:
  <form action="" method=post>
    <dl>
      <dt>Name:
      <dd><input type=text name=name size=30 value="{{ request.values.name }}">
      <dt>E-Mail:
      <dd><input type=text name=email size=30 value="{{ request.values.email }}">
    </dl>
    <p>
      <input type=submit value="Create profile">
      <input type=hidden name=next value="{{ next }}">
  </form>
  <p>
    If you don't want to proceed, you can <a href="{{ url_for('logout')
    }}">sign out</a> again.
{% endblock %}

Logging Out

The logout function is very simple, it just has to unset the openid from the session and redirect back to where the user was before:

@app.route('/logout')
def logout():
    session.pop('openid', None)
    flash(u'You were signed out')
    return redirect(oid.get_next_url())

Advanced usage

Flask-OpenID can also work with any python-openid extension. To use this, pass a list of instantiated request openid.extension.Extension objects in the extensions field of try_login(). The responses of these extensions are available during the after_login() function, as entries in resp.extensions.

Full Example

To see the full code of that example, you can download the code from github.

Changes

1.2

  • The safe_roots argument and URL security system was added.
  • The OpenID extensions system was added.

1.0

  • the OpenID object is not registered to an application which allows configuration values to be used and is also consistent with other Flask extensions.

API References

The full API reference:

class flask_openid.OpenID(app=None, fs_store_path=None, store_factory=None, fallback_endpoint=None, extension_responses=None, safe_roots=None, url_root_as_trust_root=False)

Simple helper class for OpenID auth. Has to be created in advance like a Flask object.

There are two usage modes which work very similar. One is binding the instance to a very specific Flask application:

app = Flask(__name__)
db = OpenID(app)

The second possibility is to create the object once and configure the application later to support it:

oid = OpenID()

def create_app():
    app = Flask(__name__)
    oid.init_app(app)
    return app
Parameters:
  • app – the application to register this openid controller with.
  • fs_store_path – if given this is the name of a folder where the OpenID auth process can store temporary information. If neither is provided a temporary folder is assumed. This is overridden by the OPENID_FS_STORE_PATH configuration key.
  • store_factory – alternatively a function that creates a python-openid store object.
  • fallback_endpoint – optionally a string with the name of an URL endpoint the user should be redirected to if the HTTP referrer is unreliable. By default the user is redirected back to the application’s index in that case.
  • extension_responses – a list of OpenID Extensions Response class.
  • safe_roots – a list of trust roots to support returning to
  • url_root_as_trust_root – whether to use the url_root as trust_root
after_login(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. The decorated function is passed a OpenIDResponse object.

attach_reg_info(auth_request, keys, optional_keys)

Attaches sreg and ax requests to the auth request.

Internal:
errorhandler(f)

Called if an error occurs with the message. By default 'openid_error' is added to the session so that fetch_error() can fetch that error from the session. Alternatively it makes sense to directly flash the error for example:

@oid.errorhandler
def on_error(message):
    flash(u'Error: ' + message)
fetch_error()

Fetches the error from the session. This removes it from the session and returns that error. This method is probably useless if errorhandler() is used.

get_current_url()

the current URL + next.

get_next_url()

Returns the URL where we want to redirect to. This will always return a valid URL.

get_success_url()

Return the internal success URL.

Internal:
init_app(app)

This callback can be used to initialize an application for the use with this openid controller.

New in version 1.0.

loginhandler(f)

Marks a function as login handler. This decorator injects some more OpenID required logic. Always decorate your login function with this decorator.

signal_error(msg)

Signals an error. It does this by storing the message in the session. Use errorhandler() to this method.

try_login(identity_url, ask_for=None, ask_for_optional=None, extensions=None, immediate=False)

This tries to login with the given identity URL. This function must be called from the login_handler. The ask_for and ask_for_optional`parameter can be a set of values to be asked from the openid provider, where keys in `ask_for are marked as required, and keys in ask_for_optional are marked as optional.

The following strings can be used in the ask_for and ask_for_optional parameters: aim, blog, country, dob (date of birth), email, fullname, gender, icq, image, jabber, language, msn, nickname, phone, postcode, skype, timezone, website, yahoo

extensions can be a list of instances of OpenID extension requests that should be passed on with the request. If you use this, please make sure to pass the Response classes of these extensions when initializing OpenID.

immediate can be used to indicate this request should be a so-called checkid_immediate request, resulting in the provider not showing any UI. Note that this adds a new possible response: SetupNeeded, which is the server saying it doesn’t have enough information yet to authorized or reject the authentication (probably, the user needs to sign in or approve the trust root).

class flask_openid.OpenIDResponse(resp, extensions)

Passed to the after_login function. Provides all the information sent from the OpenID provider. The profile information has to be requested from the server by passing a list of fields as ask_for to the try_login() function.

aim = None

AIM messenger address as string

blog = None

URL of blog as string

country = None

the country of the user as specified by ISO3166

date_of_birth = None

date of birth as datetime object.

email = None

the email address of the user

extensions = None

Hash of the response object from the OpenID Extension by the

fullname = None

the full name of the user

gender = None

the gender of the user (f for femail and m for male)

icq = None

icq messenger number as string

identity_url = None

the openid the user used for sign in

image = None

URL to profile image as string

jabber = None

jabber address as string

language = None

the user’s preferred language as specified by ISO639

month_of_birth = None

the month of birth of the user as integer (1 based)

msn = None

msn name as string

nickname = None

desired nickname of the user

phone = None

phone number of the user as string

postcode = None

free text that should conform to the user’s country’s postal system

skype = None

skype name as string

timezone = None

timezone string from the TimeZone database

website = None

URL of website as string

yahoo = None

yahoo messenger address as string

year_of_birth = None

the year of birth of the user as integer

flask_openid.COMMON_PROVIDERS

a dictionary of common provider name -> login URL mappings. This can be used to implement “click button to login” functionality.

Currently contains general purpose entrypoints for the following providers: google, yahoo, aol, and steam.

Fork me on GitHub