Overview

If you're new to SAML the first thing to do is get your head around it: http://en.wikipedia.org/wiki/Security_Assertion_Markup_Language. Mainly the "SAML Use Case" section.

For something at high level try this nifty video. Or this one for a longer more advanced video

Basically when a user clicks login on your site they are redirected to an external site (known as the idp) which authenticates the user and sends them back to your site (known as the sp) where they will be logged in by your app. This process is nice because you can hand over the entire authentication piece to a 3rd party. This is very useful in internal apps for companies because as soon as a user gets an active directory (or whatever) login, they now have access to all of your internal apps.

Python Libraries

As of early 2015, the best (or at least best documented) python library is python-saml (this walkthrough aligns with v2.1.2). There are others out there, this stackoverflow question does a good job describing them. The reason I'm writing this is that even with all those projects and docs, this topic still has a very high barier of entry. Hopefully I can bring that down a bit.

For this walkthrough:

  • idp = identity provider = the user manager that provides authentication (LDAP/Active Directory)
  • sp = service provider = your app

Setup

#do your virtualenv stuff...

#install
pip install flask-login
pip install python-saml

#generate key and cert
openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout sp.key -out sp.crt

Settings

Fill out your settings.json and advanced_settings.json files. You'll find more info in the main documentation (which you should read through anyway). Put them in a folder called 'saml', and in that folder create a folder called 'certs'. In the certs folder add the sp.key and sp.crt you just generated.

Metadata

This part is very important. You will need to generate metadata xml and send it to your IDP. This is important because the IDP needs to be able to verify your cert and also know which endpoints to hit. You can generate one specifically for localhost, and then also ones for other environments you have (dev/staging/production), and the IDP should (ideally) be able to handle them all.

The metadata endpoint does not stay in the app. It's used only to generate a flat xml file.

python-saml gives you a nice template for creating metadata.. But don't do it just yet, we'll do it further down.

python-saml-flask

Enter the actual code. I wanted to extract as much of the logic out of the original index.py as possible before putting anything in my views, so I rewrote all the logic into a separate extension.

Here's the extension repo. Save the main file somewhere in your repo and then import it into your app (init.py/app.py):

# setup flask-login
from flask.ext.login import LoginManager
login_manager = LoginManager()
login_manager.init_app(app)
login_manager.login_view = '/saml/login'

# setup python-saml-flask
from common.saml import SamlManager
saml_manager = SamlManager()
saml_manager.init_app(app)

# setup acs response handler
@saml_manager.login_from_acs
def login_from_acs(acs):
  # define login logic here depending on idp response
  # must call login_user() and redirect as necessary
  print acs.get('attributes')
  pass

That last method is important. After the user logins at the idp, it sends a POST back to your app. But whats in the POST data will depend on your idp. Therefore that method has to be coded by you. This method receives a SamlRequest object, your idp's response will be in the attributes variable. At some point this function might get too big and the logic would be better extracted to a seperate file and then called/imported here.

Here's an example of what one might look like:

@saml_manager.login_from_acs
def acs_login(acs):
    if acs.get('errors'):
        return jsonify({'errors': acs.get('errors')})
    elif not acs.get('logged_in'):
        return jsonify({"error": 'login failed'})
    else:
        for attr in acs.get('attributes'):
            if attr[0] == 'emailAddress':
                login_user(User.find_or_create(attr[1])) #JustInTime user creation
        if not current_user.is_authenticated():
            return {'error': 'could not find email in idp authentication response'}
    return redirect('/home')

Configs

SAML_SETTINGS_PATH

* STRING, Required
* example: /your_app_path/configs/saml
* path to a 'saml' folder which has settings.json, advanced_settings.json and a folder named certs with all certs/keys. This is one way to handle environments: /your_app_path/configs/production/saml

SAML_LOGOUT_PATH

* STRING, Optional
* example: /login
* If not set, it renders 'saml_logout_successful.html' template

Testing it by generating metadata

Add this to a view somewhere:

from app.common.saml import SamlRequest
from flask import request
@app.route('/saml/metadata')
def metadata():
  saml = SamlRequest(request)
  return saml.generate_metadata()

If you have your SAML_SETTINGS_PATH set right then that view will print out a metadata xml file which you can adjust if necessary and then send it to your idp.

Debugging

You're probably going to need to debug within the actual python-saml code to figure out your errors. I found this cool snippet on /r/python to help you do that. Put this in your bashrc/zshrc:

function pycd () { pushd `python -c "import os.path, $1; print os.path.dirname($1.__file__)"`; }

Now you can run:

pycd onelogin.saml2

And your in the virtualenv directory, from here you can add your breakpoints or subl .

Closing

Eventually if this gets some traction it could be tweaked and published as an actual flask extension, but I think the community needs to post its experiences with other IDP's to make it really useful. I'm afraid my IDP experience alone will be too specific and the code as it stands won't be 'plug-and-play' ready for other IDP's. Please fork/comment/run-with-it if you'd like to help out. It needs a test suite :)