Advertisement
Scroll to top
Read Time: 9 min

Flask is a Python-based micro web framework which allows you to write your web applications quickly and efficiently. "Micro" 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, the Flask-Login extension, and environment setup best practices using virtualenv to be followed while developing a Python application.

LDAP Server

To use LDAP, you will need a server; OpenLDAP is an open-source implementation of LDAP. Other alternatives are the Microsoft Azure Active Directory, a premium service. Since we only need a test server, there will be no need to set up a local server. We will use a publicly available LDAP testing server from Forum Systems.

Installing Dependencies

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

1
$ pip install ldap3
2
$ 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 so that it is easy to understand.

1
flask_app/
2
    my_app/
3
        - __init__.py
4
        auth/
5
            - __init__.py
6
            - models.py
7
            - views.py
8
        static/
9
            - css/
10
            - js/
11
        templates/
12
            - base.html
13
            - home.html
14
            - login.html
15
    - 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

1
from flask import Flask
2
from flask_sqlalchemy import SQLAlchemy
3
from flask_login import LoginManager
4
5
app = Flask(__name__)
6
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/test.db'
7
app.config['WTF_CSRF_SECRET_KEY'] = 'random key for form'
8
app.config['LDAP_PROVIDER_URL'] = 'ldap://ldap.forumsys.com:389/'
9
app.config['LDAP_PROTOCOL_VERSION'] = 3
10
11
db = SQLAlchemy(app)
12
13
app.secret_key = 'randon_key'
14
login_manager = LoginManager()
15
login_manager.init_app(app)
16
login_manager.login_view = 'auth.login'
17
18
ctx = app.test_request_context()
19
ctx.push()
20
21
from my_app.auth.views import auth
22
app.register_blueprint(auth)
23
24
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

1
import ldap3
2
from flask_wtf import Form
3
from wtforms import StringField,PasswordField
4
from wtforms import validators
5
6
from my_app import db, app
7
8
def get_ldap_connection():
9
    conn = ldap3.initialize(app.config['LDAP_PROVIDER_URL'])
10
    return conn
11
12
class User(db.Model):
13
    id = db.Column(db.Integer, primary_key = True)
14
    username = db.Column(db.String(100))
15
    
16
17
    def __init__(self, username, password):
18
        self.username = username
19
20
    @staticmethod
21
    def try_login(username, password):
22
        conn = get_ldap_connection()
23
        conn.simple_bind_s(
24
            'cn=%s,ou=mathematicians,dc=example,dc=com' % username,password
25
        )
26
    def is_authenticated(self):
27
        return True
28
 
29
    def is_active(self):
30
        return True
31
 
32
    def is_anonymous(self):
33
        return False
34
 
35
    def get_id(self):
36
        return self.id
37
38
class LoginForm(Form):
39
    username = StringField('Username',[validators.DataRequired()])
40
    password = PasswordField('Password',[validators.DataRequired()])    

The file above starts with the creation of a User model which contains just a username field for demonstration purposes. 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 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

1
import ldap3
2
from flask import (request,render_template,flash, redirect, url_for,
3
 Blueprint,g
4
)
5
from flask_login import (current_user,login_user,logout_user,login_required)
6
from my_app import login_manager,db
7
8
9
auth = Blueprint('auth', __name__)
10
11
from my_app.auth.models import User,LoginForm
12
13
@login_manager.user_loader
14
def load_user(id):
15
    return User.query.get(int(id))
16
17
@auth.before_request
18
def get_current_user():
19
    g.user = current_user
20
21
@auth.route('/')
22
@auth.route('/home')
23
def home():
24
    return render_template('home.html')
25
26
@auth.route('/login', methods = ['GET','POST'])
27
def login():
28
    if current_user.is_authenticated:
29
        flash('You are already logged in.')
30
        return redirect(url_for('auth.home'))
31
    form = LoginForm(request.form)
32
33
    if request.method == 'POST' and form.validate():
34
        username = request.form['username']
35
        password = request.form['password']
36
        
37
        try:
38
            User.try_login(username,password)
39
        except:
40
            flash(
41
                'Invalid username or password. Please try again.',
42
                'danger')
43
            return render_template('login.html', form=form)
44
        user = User.query.filter_by(username = username)
45
46
        if not user:
47
            user = User(username = username, password = password)
48
            db.session.add(user)
49
            db.commit()
50
        login_user(user)
51
        flash('You have successfully logged in.', 'success')
52
        return redirect(url_for('auth.home'))
53
54
    if form.errors:
55
        flash(form.errors, 'danger')
56
 
57
    return render_template('login.html', form=form)
58
        
59
60
61
@auth.route('/logout')
62
@login_required
63
def logout():
64
    logout_user()
65
    return redirect(url_for('auth.home'))
66

In the above file, the methods load_user() and get_current_user() are needed by the 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 are 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 their existing record.

Flash messages are shown to the user 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

1
<!DOCTYPE html>
2
<html lang="en">
3
  <head>
4
    <meta charset="utf-8">
5
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
6
    <meta name="viewport" content="width=device-width, initial-scale=1">
7
    <title>Flask Authentication with LDAP Tutorial</title>
8
    <link href="{{ url_for('static', filename='css/bootstrap.min.css') }}" rel="stylesheet">
9
    <link href="{{ url_for('static', filename='css/main.css') }}" rel="stylesheet">
10
  </head>
11
  <body>
12
    <div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
13
      <div class="container">
14
        <div class="navbar-header">
15
          <a class="navbar-brand" href="{{ url_for('auth.home') }}">Flask LDAP Demo</a>
16
        </div>
17
      </div>
18
    </div>
19
    <div class="container">
20
    <br/>
21
    <div>
22
      {% for category, message in get_flashed_messages(with_categories=true) %}
23
        <div class="alert alert-{{category}} alert-dismissable">
24
          <button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>
25
          {{ message }}
26
        </div>
27
      {% endfor %}
28
      </div>
29
    {% block container %}{% endblock %}
30
    </div>
31
 
32
    <!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
33
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.0.0/jquery.min.js"></script>
34
    <script src="{{ url_for('static', filename='js/bootstrap.min.js') }}"></script>
35
    {% block scripts %}
36
    {% endblock %}
37
  </body>
38
</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

1
{% extends 'base.html' %}
2
 
3
{% block container %}
4
  <h1>Welcome to the Flask-LDAP Authentication Demo</h1>
5
  {% if current_user.is_authenticated %}
6
    <h3>Hey {{ current_user.username }}!!</h3>
7
    <a href="{{ url_for('auth.logout') }}">Click here to logout</a>
8
  {% else %}
9
  Click here to <a href="{{ url_for('auth.login') }}">login with LDAP</a>
10
  {% endif %}
11
{% 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, they're greeted with the username and shown a message to log out. Otherwise, the user is shown a message to log in, with a link to the login page.

flask_app/my_app/templates/login.html

1
{% extends 'home.html' %}
2
 
3
{% block container %}
4
  <div class="top-pad">
5
    <form
6
        method="POST"
7
        action="{{ url_for('auth.login') }}"
8
        role="form">
9
      {{ form.csrf_token }}
10
      <div class="form-group">{{ form.username.label }}: {{ form.username() }}</div>
11
      <div class="form-group">{{ form.password.label }}: {{ form.password() }}</div>
12
      <button type="submit" class="btn btn-default">Submit</button>
13
    </form>
14
  </div>
15
{% 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:

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

Now just execute from the command line:

1
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 users:

  • riemann
  • gauss
  • euler
  • euclid

All user passwords are password.

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

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.