Add LDAP Aunthentication

This commit is contained in:
2026-08-06 18:17:33 +05:30
parent e7805b71a8
commit b380ed510b
11 changed files with 169 additions and 225 deletions

View File

@@ -1,130 +1,72 @@
from ldap3 import (
Server,
Connection,
ALL,
NTLM,
SIMPLE,
SUBTREE
)
from flask import current_app
from app.config import Config
from ldap3 import Server, Connection, ALL, SUBTREE
from ldap3.core.exceptions import LDAPException
class LDAPService:
"""
LDAP / Active Directory Authentication Service
Handles authentication against an LDAP / OpenLDAP server using the
standard "search + bind" pattern:
1. Bind with a service/admin account just to SEARCH for the user's DN.
2. Re-bind using that DN + the password the user typed, to verify it.
The user's typed password is only ever used in step 2, never sent
anywhere else.
"""
@staticmethod
def authenticate(username, password):
"""
Authenticate LDAP User
Returns:
{
"success": True,
"user": {
"username": "...",
"name": "...",
"email": "..."
}
}
OR
{
"success": False,
"message": "Invalid username or password"
}
"""
if not username or not password:
return {
"success": False,
"message": "Username and Password are required."
}
return None
server = Server(current_app.config["LDAP_SERVER"], get_info=ALL)
# --- Step 1: bind as the admin/service account to search the directory ---
try:
# -----------------------------------
# LDAP SERVER
# -----------------------------------
server = Server(
Config.LDAP_SERVER,
port=Config.LDAP_PORT,
use_ssl=Config.LDAP_USE_SSL,
get_info=ALL
)
# -----------------------------------
# Login Format
#
# username@domain.com
# -----------------------------------
user_dn = f"{username}@{Config.LDAP_DOMAIN}"
conn = Connection(
admin_conn = Connection(
server,
user=user_dn,
password=password,
authentication=SIMPLE,
auto_bind=True
user=current_app.config["LDAP_BIND_DN"],
password=current_app.config["LDAP_BIND_PASSWORD"],
auto_bind=True,
)
except LDAPException as e:
current_app.logger.error(f"LDAP service account bind failed: {e}")
return None
# -----------------------------------
# Search User
# -----------------------------------
search_filter = f"(sAMAccountName={username})"
conn.search(
search_base=Config.LDAP_SEARCH_BASE,
# --- Step 2: find the user's real DN + profile attributes ---
try:
search_filter = current_app.config["LDAP_SEARCH_FILTER"].format(username=username)
admin_conn.search(
search_base=current_app.config["LDAP_BASE_DN"],
search_filter=search_filter,
search_scope=SUBTREE,
attributes=[
"displayName",
"mail",
"givenName",
"sn",
"cn"
]
attributes=["cn", "mail", "uid"],
)
except LDAPException as e:
current_app.logger.error(f"LDAP search failed for '{username}': {e}")
admin_conn.unbind()
return None
display_name = username
email = ""
if not admin_conn.entries:
current_app.logger.warning(f"LDAP user not found: {username}")
admin_conn.unbind()
return None
if conn.entries:
entry = admin_conn.entries[0]
user_dn = entry.entry_dn
name = str(entry.cn) if "cn" in entry and entry.cn.value else username
email = (
str(entry.mail)
if "mail" in entry and entry.mail.value
else f"{username}@{current_app.config['LDAP_DOMAIN']}"
)
admin_conn.unbind()
entry = conn.entries[0]
# --- Step 3: the actual auth check - bind AS the user with their password ---
try:
user_conn = Connection(server, user=user_dn, password=password, auto_bind=True)
user_conn.unbind()
except LDAPException as e:
current_app.logger.warning(f"LDAP authentication failed for '{username}': {e}")
return None
if "displayName" in entry:
display_name = str(entry.displayName)
if "mail" in entry:
email = str(entry.mail)
conn.unbind()
current_app.logger.info(
f"LDAP Login Success : {username}"
)
return {
"success": True,
"user": {
"username": username,
"name": display_name,
"email": email
}
}
except Exception as ex:
current_app.logger.warning(
f"LDAP Login Failed : {username} : {str(ex)}"
)
return {
"success": False,
"message": "Invalid Username or Password."
}
return {"username": username, "name": name, "email": email}

View File

@@ -1,6 +1,7 @@
from flask import current_app
from app.models.user_model import User
from app.services.db_service import db
from flask import current_app
from app.services.ldap_service import LDAPService
class UserService:
@@ -9,21 +10,57 @@ class UserService:
if User.query.filter_by(email=email).first():
return None
user = User(name=name, email=email)
user = User(name=name, email=email, auth_source="local")
user.set_password(password)
db.session.add(user)
db.session.commit()
current_app.logger.info("User list viewed")
return user
@staticmethod
def validate_login(email, password):
user = User.query.filter_by(email=email).first()
def validate_login(identifier, password):
"""
identifier = whatever was typed in the login form. Can be an email
(local users) or an LDAP username, depending on USE_LDAP_AUTH.
"""
if current_app.config.get("USE_LDAP_AUTH"):
ldap_user = UserService._validate_ldap_login(identifier, password)
if ldap_user:
return ldap_user
return None
user = User.query.filter_by(email=identifier).first()
if user and user.check_password(password):
return user
return None
@staticmethod
def _validate_ldap_login(username, password):
ldap_info = LDAPService.authenticate(username, password)
if not ldap_info:
return None
return UserService._get_or_create_ldap_user(ldap_info)
@staticmethod
def _get_or_create_ldap_user(ldap_info):
"""
LDAP is the source of truth for the password. We still keep a row in
our local `users` table (no password) so the rest of the app - which
expects a User with an id - keeps working unchanged.
"""
user = User.query.filter_by(email=ldap_info["email"]).first()
if user is None:
user = User(
name=ldap_info["name"],
email=ldap_info["email"],
auth_source="ldap",
)
db.session.add(user)
db.session.commit()
elif user.name != ldap_info["name"]:
user.name = ldap_info["name"]
db.session.commit()
return user
@staticmethod
def get_all_users():
return User.query.all()