// LDAP simple-bind handler. Verifies the typed password against the user's // stored bcrypt hash via Saltcorn's User.checkPassword (never stores/echoes // plaintext), or against the configured service account (constant-time compare, // for the search-then-bind flow). Denies anonymous, oversized, and cross-tenant // binds, and rate-limits failures per IP. The tenant is derived from the bind // DN base (dc=,...) and all lookups run inside runWithTenant. const nodeCrypto = require("crypto"); const User = require("@saltcorn/data/models/user"); const vendor = require("./vendor"); const harden = require("./harden"); const tenant = require("./tenant"); const serviceAccount = require("./serviceAccount"); const { uidFromDn } = require("./dn"); const MAX_CRED_LEN = 1024; const ipOf = (req) => { return (req.connection && req.connection.remoteAddress) || "unknown"; }; const dnEquals = (a, b) => { return String(a).replace(/\s+/g, "").toLowerCase() === String(b).replace(/\s+/g, "").toLowerCase(); }; const constantTimeEqual = (a, b) => { const ba = Buffer.from(String(a)); const bb = Buffer.from(String(b)); if (ba.length !== bb.length) { return false; } return nodeCrypto.timingSafeEqual(ba, bb); }; const handler = async (req, res, next) => { const ip = ipOf(req); if (harden.isLocked(ip)) { return next(new vendor.InvalidCredentialsError()); } try { const creds = req.credentials || ""; // Deny anonymous (no DN / empty password) and absurdly long credentials. if (!req.dn || req.dn.length === 0 || creds === "" || creds.length > MAX_CRED_LEN) { harden.recordFail(ip); return next(new vendor.InvalidCredentialsError()); } const t = tenant.resolveTenant(tenant.tenantFromDn(req.dn)); if (t.deny) { harden.recordFail(ip); return next(new vendor.InvalidCredentialsError()); } const dnStr = req.dn.toString(); const authed = await tenant.withTenant(t.tenant, async () => { // Service-account bind (search-then-bind binder). const svc = await serviceAccount.getServiceAccount(); if (svc && svc.dn && dnEquals(dnStr, svc.dn)) { return constantTimeEqual(creds, svc.password); } // User bind. const email = uidFromDn(req.dn); const user = email ? await User.findOne({ email: email }) : null; if (!user || user.disabled || !user.checkPassword(creds)) { return false; } return true; }); if (!authed) { harden.recordFail(ip); return next(new vendor.InvalidCredentialsError()); } harden.recordSuccess(ip); res.end(); return next(); } catch (e) { // eslint-disable-next-line no-console console.error("[saltcorn-idp] ldap bind error:", e); harden.recordFail(ip); return next(new vendor.InvalidCredentialsError()); } }; module.exports = { handler };