Flask Authentication With LDAP

Flask is a Python-based micro web framework which allows you to write your web applications quickly and efficiently. By micro, it doesn’t mean that Flask lacks in functionality. It simply refers to the fact that Flask has kept its core small and highly extensible.

LDAP (Lightweight Directory Access Protocol) can have different meanings for different people depending upon their usage. It is an Internet Protocol for looking up contact information about users, information about certificates, network pointers, etc., from a server where the data is stored in a directory style structure. Its most popular use is to provide a “single sign on” where a user can access multiple applications by logging in once as the password would be shared across services.

In this tutorial, I will take you through how to implement authentication of users in your Flask application using LDAP. To demonstrate this, I will create a small application with a home page and a login page. The user would need to enter the login details on the login page. If the credentials entered by the user are successfully authenticated on the provided LDAP server, the user will be logged in. If not, the user will be shown an appropriate message.

I assume that you have a basic understanding of Flask, LDAP, Flask-Login extension and environment setup best practices using virtualenv to be followed while developing a Python application.

Installing Dependencies

The following packages need to installed for the application that we’ll be developing.

$ pip install python-ldap
$ pip install flask-wtf flask-sqlalchemy flask-login

The above commands should install all the required packages that are needed for this application to work.

Application Structure

First the application needs to be structured in a way that it is easy to understand.

flask_app/
    my_app/
        - __init__.py
        auth/
            - __init__.py
            - models.py
            - views.py
        static/
            - css/
            - js/
        templates/
            - base.html
            - home.html
            - login.html
    - run.py

All the files will be discussed below. The static folder contains the standard Bootstrap CSS and JS files.

The Application Itself

First, the configuration file needs to be written:

flask_app/my_app/__init__.py

from flask import Flask
from flask.ext.sqlalchemy import SQLAlchemy
from flask.ext.login import LoginManager


app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/test.db'
app.config['WTF_CSRF_SECRET_KEY'] = 'random key for form'
app.config['LDAP_PROVIDER_URL'] = 'ldap://ldap.testathon.net:389/'
app.config['LDAP_PROTOCOL_VERSION'] = 3
db = SQLAlchemy(app)

app.secret_key = 'some_random_key'

login_manager = LoginManager()
login_manager.init_app(app)
login_manager.login_view = 'login'

from my_app.auth.views import auth
app.register_blueprint(auth)

db.create_all()

In the file above, the application has been configured with different options as needed by the extensions as well as by the LDAP configuration. This is followed by the initialisation of the extensions and finally creation of the database. 

The last statement creates a new database at the location provided against SQLALCHEMY_DATABASE_URI if a database does not already exist at that location, otherwise it loads the application with the same database.

flask_app/my_app/auth/models.py

import ldap
from flask_wtf import Form
from wtforms import TextField, PasswordField
from wtforms.validators import InputRequired
from my_app import db, app


def get_ldap_connection():
    conn = ldap.initialize(app.config['LDAP_PROVIDER_URL'])
    return conn


class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(100))

    def __init__(self, username, password):
        self.username = username

    @staticmethod
    def try_login(username, password):
        conn = get_ldap_connection()
        conn.simple_bind_s(
            'cn=%s,ou=Users,dc=testathon,dc=net' % username,
            password
        )

    def is_authenticated(self):
        return True

    def is_active(self):
        return True

    def is_anonymous(self):
        return False

    def get_id(self):
        return unicode(self.id)


class LoginForm(Form):
    username = TextField('Username', [InputRequired()])
    password = PasswordField('Password', [InputRequired()])

The file above starts with the creation of a User model which contains just a username field for demonstration purpose. You can add as many fields as needed according to the context of the application. 

The methods is_authenticated()is_active()is_anonymous() and get_id() are needed by the Flask-Login extension. The try_login() method does the actual authentication process by first creating a connection with the LDAP server and then using the username and password to log in the server by creating a simple bind. 

The first argument taken by the simple_bind_s() method is the DN, which is provided by the LDAP server and varies with the LDAP record configuration. The form handling is taken care of by the LoginForm, which extends the Form class provided by Flask-WTForms.

flask_app/my_app/auth/views.py

`import ldap
from flask import request, render_template, flash, redirect, \
    url_for, Blueprint, g
from flask.ext.login import current_user, login_user, \
    logout_user, login_required
from my_app import login_manager, db
from my_app.auth.models import User, LoginForm

auth = Blueprint('auth', __name__)


@login_manager.user_loader
def load_user(id):
    return User.query.get(int(id))


@auth.before_request
def get_current_user():
    g.user = current_user


@auth.route('/')
@auth.route('/home')
def home():
    return render_template('home.html')


@auth.route('/login', methods=['GET', 'POST'])
def login():
    if current_user.is_authenticated():
        flash('You are already logged in.')
        return redirect(url_for('auth.home'))

    form = LoginForm(request.form)

    if request.method == 'POST' and form.validate():
        username = request.form.get('username')
        password = request.form.get('password')

        try:
            User.try_login(username, password)
        except ldap.INVALID_CREDENTIALS:
            flash(
                'Invalid username or password. Please try again.',
                'danger')
            return render_template('login.html', form=form)

        user = User.query.filter_by(username=username).first()

        if not user:
            user = User(username, password)
            db.session.add(user)
            db.session.commit()
        login_user(user)
        flash('You have successfully logged in.', 'success')
        return redirect(url_for('auth.home'))

    if form.errors:
        flash(form.errors, 'danger')

    return render_template('login.html', form=form)


@auth.route('/logout')
@login_required
def logout():
    logout_user()
    return redirect(url_for('auth.home'))

In the above file, the methods load_user() and get_current_user() are needed by Flask-Login extension. Next are the handlers for our application, which are decorated by their respective routes.

home() just renders the home page for the user. The content of the home page is determined by the template flask_app/my_app/templates/home.html, which we’ll discuss shortly.

The handler of primary interest is login() as it handles the complete login process. If a logged in user tries to access this page, the page will automatically redirect to the home page. Otherwise, the login process will begin where the LDAP username and password of the user is taken as form input from flask_app/my_app/templates/login.html

Using these credentials, the application tries to authenticate the user from the LDAP server provided in the configuration we saw earlier. If the user is authenticated, the application creates a new record for the user if a first time user is accessing the application, otherwise it just logs the user in with the existing record of the user.

Flash messages are shown to the user as and when required to keep the user engaged with the application.

logout() hander simply clears the session of the currently logged in user as a result of which the user is logged out.

flask_app/my_app/templates/base.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Flask Authentication with LDAP Tutorial</title>
    <link href="{{ url_for('static', filename='css/bootstrap.min.css') }}" rel="stylesheet">
    <link href="{{ url_for('static', filename='css/main.css') }}" rel="stylesheet">
  </head>
  <body>
    <div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
      <div class="container">
        <div class="navbar-header">
          <a class="navbar-brand" href="{{ url_for('auth.home') }}">Flask LDAP Demo</a>
        </div>
      </div>
    </div>
    <div class="container">
    <br/>
    <div>
      {% for category, message in get_flashed_messages(with_categories=true) %}
        <div class="alert alert-{{category}} alert-dismissable">
          <button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>
          {{ message }}
        </div>
      {% endfor %}
      </div>
    {% block container %}{% endblock %}
    </div>

    <!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.0.0/jquery.min.js"></script>
    <script src="{{ url_for('static', filename='js/bootstrap.min.js') }}"></script>
    {% block scripts %}
    {% endblock %}
  </body>
</html>

Above is the base file which contains the header, footer and other basic components which remain common throughout the application. This helps in keeping the templates very modular and easy to understand as each template only contains the code relevant to its handler and functionality. 

Even though all the common components are defined here, I have added an empty block for scripts which can be extended in any template which inherits base.html. Notice how the flashing of messages is being done above and how the Bootstrap CSS classes are dynamically being played with to make the flash message alert box appropriately styled.

The container block will be extended by the rest of the templates to add their respective contents.

flask_app/my_app/templates/home.html

{% extends 'base.html' %}

{% block container %}
  <h1>Welcome to the Flask-LDAP Authentication Demo</h1>
  {% if current_user.is_authenticated() %}
    <h3>Hey {{ current_user.username }}!!</h3>
    <a href="{{ url_for('auth.logout') }}">Click here to logout</a>
  {% else %}
  Click here to <a href="{{ url_for('auth.login') }}">login with LDAP</a>
  {% endif %}
{% endblock %}

Notice how the base template has been extended and content for the home page has been added inside the block container.

If the user is logged in, the user is greeted with the username and shown a message to log out. Otherwise, the user is shown a message to log in with the link to login page.

flask_app/my_app/templates/login.html

{% extends 'home.html' %}

{% block container %}
  <div class="top-pad">
    <form
        method="POST"
        action="{{ url_for('auth.login') }}"
        role="form">
      {{ form.csrf_token }}
      <div class="form-group">{{ form.username.label }}: {{ form.username() }}</div>
      <div class="form-group">{{ form.password.label }}: {{ form.password() }}</div>
      <button type="submit" class="btn btn-default">Submit</button>
    </form>
  </div>
{% endblock %}

The login template simply contains a form with fields for username and password.

Running the Application

To run the application, execute the script run.py. The contents of this script are:

from my_app import app
app.run(debug=True)

Now just execute from the command line:

python run.py

Finally open up http://127.0.0.1:5000/ in your preferred browser and see your application in action. Everything should be self-explanatory. In order to log in, you can use the following credentials:

  • stuart/stuart
  • carol/carol
  • john/john

I have used the testathon server which has been put up by Stuart Lewis.

Conclusion

Over the course of this tutorial, we built a small but effective web application using Flask with the help of the Flask-Login extension. This application simply takes a username and password and authenticates the user against the LDAP server provided. Use your imagination to tweak and extend the application as per your needs.

Resources

Leave a Reply

Your email address will not be published. Required fields are marked *