Skip to main content
  1. Posts/

FLASK with LDAP Authentication against Active Directory and group authorization for specific pages

·457 words·3 mins·
netdevops blog python flask ldap active_directory
Maximilian Thoma
Author
Maximilian Thoma
network engineer
Table of Contents

This is an example of how to implement authentication for a FLASK website using Active Directory with LDAP. Additionally, there is a decorator that allows restricting access to specific subpages to individual or multiple AD groups. The following PIP packages are required for the example: flask, flask-login, ldap3. The functions, etc., are documented in the code.

Code
#

#!/usr/bin/env python3

# FLASK with LDAP3 authentication against active directory and authorization check for group membership
# Written by Maximilian Thoma 2023
# Visit: https://lanbugs.de for more ...

from functools import wraps
from flask import Flask, request, redirect, url_for, render_template, abort
from flask_login import LoginManager, UserMixin, login_user, login_required, logout_user, current_user
from ldap3 import Server, Connection, SUBTREE, SIMPLE

# LDAP Settings
LDAP_USER = "CN=LDAP Bind,CN=Users,DC=ad,DC=local"
LDAP_PASS = "SuperSecret12345567"
LDAP_SERVER = "ldap://ad01.ad.local"
AD_DOMAIN = "ADLOCAL"
SEARCH_BASE = "CN=Users,DC=ad,DC=local"

# Init Flask
app = Flask(__name__)
app.secret_key = "ThisSecretIsVeryWeakDoItBetter"

# Init LoginManager
login_manager = LoginManager()
login_manager.login_view = "login"
login_manager.init_app(app)


class User(UserMixin):
    """
    The user model
    """
    def __init__(self, username):
        self.id = username
        self.groups = []


def authenticate_ldap(username, password):
    """
    Check authentication of user against AD with LDAP
    :param username: Username
    :param password: Password
    :return: True is authentication is successful, else False
    """
    server = Server(LDAP_SERVER, use_ssl=True)

    try:
        with Connection(server,
                        user=f'{AD_DOMAIN}\\{username}',
                        password=password,
                        authentication=SIMPLE,
                        check_names=True,
                        raise_exceptions=True) as conn:
            if conn.bind():
                print("Authentication successful")
                return True
    except Exception as e:
        print(f"LDAP authentication failed: {e}")
    return False


def get_user_groups(username):
    """
    Connect to LDAP and query for all groups
    :param username: Username
    :return: List of group names
    """
    server = Server(LDAP_SERVER, use_ssl=True)

    with Connection(server,
                    user=LDAP_USER,
                    password=LDAP_PASS,
                    auto_bind=True) as conn:
        search_filter = f'(sAMAccountName={username})'
        conn.search(search_base=SEARCH_BASE,
                    search_filter=search_filter,
                    attributes=['memberOf'],
                    search_scope=SUBTREE)

        if conn.entries:
            user_entry = conn.entries[0]
            group_dns = user_entry.memberOf

            group_names = [group.split(',')[0].split('=')[1] for group in group_dns]

            return group_names

    return []


@login_manager.user_loader
def load_user(user_id):
    """
    The user_loader of flask-login, this will load Usermodel and the groups from AD
    :param user_id: Username
    :return: user object
    """
    user = User(user_id)
    user.groups = get_user_groups(user_id)
    return user


def group_required(groups):
    """
    Decorator to check group membership
    :param groups: list of groups which are allowed to see the site
    """
    def decorator(func):
        @wraps(func)
        def decorated_function(*args, **kwargs):

            for g in groups:
                if current_user.is_authenticated and g in current_user.groups:
                    return func(*args, **kwargs)

            abort(403)
        return decorated_function
    return decorator


@app.route('/login', methods=['GET', 'POST'])
def login():
    """
    Login page
    """
    if request.method == 'POST':
        username = request.form['username']
        password = request.form['password']

        if authenticate_ldap(username, password):
            user = User(username)
            login_user(user)
            return redirect(url_for('user_panel'))

    return render_template('login.html')


@app.route('/logout')
@login_required
def logout():
    """
    Logout page
    """
    logout_user()
    return redirect(url_for('login'))


@app.route('/admin')
@login_required
@group_required(["p_admin"])
def admin_panel():
    """
    Protected admin panel, only users of group p_admin are allowed to see the page 
    """
    return 'Admin Panel'


@app.route('/user')
@login_required
@group_required(["p_user", "p_admin"])
def user_panel():
    """
    Protected user panel, only users of group p_user and p_admin are allowed to see the page 
    """
    return 'User Panel'


if __name__ == "__main__":
    app.run(debug=True)

Related

Merge subnet lists
·80 words·1 min
netdevops blog python subnet cidr merge
This is a small python snippet to merge multiple subnet lists mixed with single ip addresses and CIDRs.
APIFlask Webhook Listener for Netbox
·258 words·2 mins
netdevops blog netbox python api apiflask
This little code snippet is the base of my Netbox Webhook Listener written in APIFlask.
Ansible: Disable / Enable proxy for single task
·152 words·1 min
netdevops blog ansible proxy tower
In my AWX Tower, I have globally set up a proxy.