Files
xamxam/docs/LDAP_SPEC.md
Pontoporeia cf9bd5cd5d feat: require 3 mots-clés in partage, language asterisk toggle, admin auto-save checkboxes
- tag-search: add minTags/required params, counter shows red if < 3, accent if ≥ 3
- form.php: pass minTags=3 for partage mode keywords
- checkbox-list: support labelHtml for raw HTML label with targetable asterisk span
- language-autre-fragment: OOB swap updates #languages-required-asterisk when autre pills change
- language-search: client-side update #languages-required-asterisk on pill add/remove
- contenus.php: replace 3 form+submit-button fieldsets with HTMX auto-save checkboxes
- settings.php: detect HX-Request header, return OOB CSRF token updates, skip redirect
2026-05-19 00:08:06 +02:00

7.6 KiB

LDAP Authentication Specification for XAMXAM Admin

Current state

Two-layer authentication guards the /admin/ area:

Layer Mechanism Where
1 (nginx) auth_basic against /etc/nginx/.htpasswd-xamxam nginx/xamxam.conf
2 (PHP) AdminAuth — bcrypt hash in site_settings.admin_password_hash app/src/AdminAuth.php, app/public/admin/login.php, app/public/admin/actions/account.php

Layer 1 controls the browser's Basic Auth dialog. Layer 2 provides a PHP session gate and a fallback login form. When both layers share the same password, the user is authenticated transparently (nginx passes PHP_AUTH_PW to PHP, AdminAuth verifies it against the DB hash).

Goal

Replace both layers with LDAP-based authentication while preserving the defence-in-depth structure and the transparent user experience (single sign-on via the browser's Basic Auth dialog, no PHP login form unless fallback).

Required information from IT

# Item Example / format
1 LDAP server URL ldaps://ldap.erg.be:636 or ldap://ldap.erg.be:389
2 Base DN dc=erg,dc=be
3 Bind DN (service / search account) cn=svc-xamxam,ou=services,dc=erg,dc=be
4 Bind password (secret — read-only account is sufficient)
5 User search filter (&(uid=%s)(memberOf=cn=admin-xamxam,ou=groups,dc=erg,dc=be))%s is the username entered in the Basic Auth dialog
6 Group membership mechanism memberOf attribute (AD-style) or member/uniqueMember on the group entry (OpenLDAP-style)
7 Username attribute Typically uid (OpenLDAP) or sAMAccountName (AD). What attribute should the user type in the auth dialog?
8 TLS certificate If ldaps:// is used and the certificate is self-signed, provide the CA certificate (PEM). Otherwise confirm it's a publicly-trusted cert.
9 Admin group DN/CN The exact DN or CN that grants admin access (e.g. cn=xamxam-admins,ou=groups,dc=erg,dc=be). If there's no group yet, what should it be named?

Architecture

Browser                    Nginx                       LDAP daemon              LDAP server
  │                          │                              │                       │
  │─ GET /admin/ ──────────►│                              │                       │
  │◄── 401 WWW-Authenticate │                              │                       │
  │─ GET /admin/ + Basic ──►│                              │                       │
  │                         │─ POST /auth-ldap ───────────►│                       │
  │                         │   (proxy Authorization hdr)  │─ ldap_bind ──────────►│
  │                         │                              │◄── success ───────────│
  │                         │                              │─ ldap_search ────────►│
  │                         │                              │◄── group check OK ────│
  │                         │◄── 200 OK ──────────────────│                       │
  │                         │─ forward to PHP ────────────►                        │
  │                         │                              │                       │
  │◄── admin page ─────────│                                                       │

Option A — nginx-ldap-auth daemon (preferred)

  • Drop-in replacement for auth_basic / .htpasswd using nginx's auth_request module
  • A small Python 3 daemon (nginx-ldap-auth) runs at 127.0.0.1:8888
  • Configured via /etc/nginx-ldap-auth.conf (JSON or YAML)
  • Nginx proxies the Authorization header to the daemon; daemon binds to LDAP, checks group membership, returns 200 or 403
  • The PHP AdminAuth layer remains — it receives PHP_AUTH_PW from nginx, can verify the username against LDAP group membership, and establish the PHP session

Nginx config (add to location ^~ /admin/):

location ^~ /admin/ {
    # Replace auth_basic + auth_basic_user_file with:
    auth_request /auth-ldap;
    auth_request_set $saved_set_cookie $upstream_http_set_cookie;
    add_header Set-Cookie $saved_set_cookie;

    # Client-facing Basic Auth challenge (so the browser asks for credentials)
    satisfy any;
    # Fallback: if auth_request returns 401, challenge
    error_page 401 = @ldap_challenge;

    # Keep: rate limiting, CSP, PHP handling, security headers
    limit_req zone=admin burst=20 nodelay;
    # ... rest as-is ...
}

# Internal endpoint — delegates to LDAP daemon
location = /auth-ldap {
    internal;
    proxy_pass http://127.0.0.1:8888;
    proxy_pass_request_body off;
    proxy_set_header Content-Length "";
    proxy_set_header Authorization $http_authorization;
}

# Trigger browser Basic Auth dialog when LDAP returns 401
location @ldap_challenge {
    add_header WWW-Authenticate 'Basic realm="Admin Access - XAMXAM"';
    return 401;
}

Option B — ngx_http_auth_ldap_module (native nginx module)

  • Requires recompiling nginx with this third-party module
  • Simpler config: auth_ldap "XAMXAM Admin"; auth_ldap_servers { ... }
  • Less flexible; harder to debug

Option C — PHP-only LDAP (no nginx layer)

  • Remove nginx auth entirely
  • AdminAuth::requireLogin() does ldap_bind() + group check directly in PHP
  • Simpler nginx config, but no nginx-level gate
  • Browser auth dialog still possible via PHP sending WWW-Authenticate header

After LDAP is working: cleanup checklist

Step File(s) affected Action
1 app/src/AdminAuth.php Remove getStoredHash(), setPasswordHash(), removePasswordHash(), hasPassword(), verifyHash(). Keep requireLogin(), isAuthenticated(), login(), logout() — adapt them to LDAP group check.
2 app/public/admin/login.php Remove entirely (no more PHP login form).
3 app/public/admin/actions/account.php Remove entirely (no more password CRUD).
4 app/templates/admin/login.php Remove template file.
5 app/templates/admin/parametres.php Remove the "Compte administrateur" <section> (password set/change/delete UI).
6 app/public/admin/parametres.php Remove AdminAuth::hasPassword() call and related variables.
7 app/templates/admin/account.php Remove if only used for password management.
8 nginx/xamxam.conf Remove auth_basic and auth_basic_user_file lines from the admin location block.
9 Database Remove admin_password_hash row from site_settings table (manual or migration).
10 app/bootstrap.php Remove legacy ADMIN_PASSWORD_HASH constant reference if present.

Dependencies to install

  • Option A: Python 3, python3-ldap (or pip install python-ldap), nginx-ldap-auth daemon
  • Option B: nginx recompiled with ngx_http_auth_ldap_module
  • Option C: PHP ldap extension (php8.4-ldap or apt install php-ldap)

Notes

  • The AdminAuth PHP layer should remain even after LDAP is implemented — it provides session persistence, logout, CSRF integration, and the admin audit log identity.
  • The LDAP daemon/nginx layer handles authentication (who are you?). The PHP AdminAuth layer handles session management (are you still you?).
  • If IT provides a dedicated admin group, access control is centralised: adding/removing an admin is a single LDAP operation, no need to touch the server.