91 lines
3.1 KiB
JavaScript
91 lines
3.1 KiB
JavaScript
// 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=<tenant>,...) 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
|
|
};
|