reactjsnext.jsnext-authldapjs

res.on("searchEntry") from Ldap.js does not returns result in Next.js Production mode


Need some help and advice with my problem. In my Next.js application (14.1) I did authorization with next-auth (5.0.0) and ldap.js (3.0.7). Process goes as follow:

Important: all works just fine while in dev mode. But when build and run npm run start, "bind" goes fine, and then during client.search proces stops. The last console.log I am getting from res.on("searchRequest") and nothing after.

Does any body have idea why? Why it works on dev mode, but not in prod ?

Server action:

export const logInAction = async (prevState, formData) => {
  const { username, password } = Object.fromEntries(formData);
  if (username === "" || password === "") {
    return { error: "Wrong" };
  }
  try {
    await signIn("credentials", { username, password });
  } catch (error) {
    if (error.type?.includes("CredentialsSignin")) {
      return { error: "Wrong" };
    }

    throw error;
  }
};

next-auth logic:

const login = async (credentials) => {
  try {
    const userInfo = await call(credentials.username, credentials.password);
    console.log(userInfo);

    return user;
  } catch (error) {
    console.log("error in login function", error);
    throw new Error("Failed to login");
  }
};

export const {
  handlers: { GET, POST },
  auth,
  signIn,
  signOut,
} = NextAuth({
  ...authConfig,
  providers: [
    CredentialsProvider({
      async authorize(credentials) {
        try {
          const user = await login(credentials);

          return user;
        } catch (error) {
          console.log("error in autorize:", error);
          return null;
        }
      },
    }),
  ],
  callbacks: {
    async signIn({ user, account, profile }) {
      //doing some logic
    },
    ...authConfig.callbacks,
  },
})

ldpajs logic:

const ldap = require("ldapjs");

const createClient = () =>
  ldap.createClient({
    url: process.env.URL,
    tlsOptions: { rejectUnauthorized: false }, 
    reconnect: true, 
  });

let client;

const ADLoginUserCheck = async (user, password) => {
  return new Promise((resolve, reject) => {
    client = createClient();
    client.bind(`${user}${process.env.URL}`, password, (err, auth) => {
      if (err) {
        console.log("Reject error...");
        reject(err);
      } else {
        console.log("all ok...");
        resolve(true);
      }
    });
  });
};

const getUserDetails = async (user) => {
  const opts = {
    filter: `(sAMAccountName=${user})`,
    scope: "sub",
     attributes: [],
  };
  return new Promise((resolve, reject) => {
    let userData;
    client.search(`${process.env.URL}`, opts, (err, res) => {
      if (err) {
        console.log("Error in search:", err);
        reject(err);
      } else {
        res.on("searchRequest", (searchRequest) => {
          console.log("65", searchRequest.messageId);
        });
        res.on("searchEntry", (entry) => {
          console.log("68", entry.pojo.attributes);
          userData = entry.pojo.attributes;
        });
        res.on("searchReference", (referral) => {
          console.log("72", referral);
        });
        res.on("error", (err) => {
          console.log("75", err)
        });
        res.on("end", async (result) => {
          //   console.log("userDAta", userData);
          resolve({ userData });
          client.destroy();
        });
      }
    });
  });
};

const top = async (user, password) => {
  const isCorrenctCredentials = await ADLoginUserCheck(user, password);
  let userDetails;
  if (isCorrenctCredentials) {
    userDetails = await getUserDetails(user);
  }
  return userDetails;
};

const call = async (user, password) => {
  top(user, password).then((res) => {
    return res;
  });
};
module.exports = call

Solution

  • Try to fix the issue by adding this setting into next config (it works for me also):

    /** @type {import('next').NextConfig} */
    const nextConfig = {
      experimental: {
        serverComponentsExternalPackages: ["ldapjs"],
      },
    };
    
    module.exports = nextConfig;

    Incase that would not help I am pasting also some thought from other developer (https://github.com/ldapjs/node-ldapjs/issues/967) The search functionality behaved unexpectedly, not returning any results from the searchEntry event.

    Through some troubleshooting, I've begun to suspect that the issue may stem from the production build process. It seems possible that class names are being altered during minification, potentially affecting the ldapjs library's ability to correctly perform checks.

    In my logs, I noticed the class name change in the end event from SearchResultDone in development to a minified version like s in production, indicating the renaming of classes during the build process.

    Specifically, in client.js, the issue seems to involvehow event names are generated and used:

    if (msg instanceof SearchEntry || msg instanceof SearchReference) {
      let event = msg.constructor.name
      // Generate the event name for the event emitter, i.e., "searchEntry"
      // and "searchReference".
      event = (event[0].toLowerCase() + event.slice(1)).replaceAll('Result', '')
      return sendResult(event, msg)
    } else {
      tracker.remove(message.messageId)
      // Potentially mark client as idle
      self._updateIdle()
      if (msg instanceof LDAPResult) {
        if (msg.status !== 0 && expect.indexOf(msg.status) === -1) {
          return sendResult('error', errors.getError(msg))
        }
        return sendResult('end', msg)
      } else if (msg instanceof Error) {
        return sendResult('error', msg)
      } else {
        return sendResult('error', new errors.ProtocolError(msg.type))
      }
    }
    

    This behavior suggests that the process of generating event names based on class names (which are altered in the production build) could lead to events being dispatched with unexpected names. As a temporary workaround, I've modified the Client.prototype._sendSocket method directly to ensure that the searchEntry event is emitted without relying on the class name.

    const ldap = require("ldapjs");
    
    ldap.Client.prototype._sendSocket = function (
      message,
      expect,
      emitter,
      callback
    ) {
      const conn = this._socket;
      const tracker = this._tracker;
      const log = this.log;
      const self = this;
      let timer = false;
      let sentEmitter = false;
    
      function sendResult(event, obj) {
        if (event === "error") {
          self.emit("resultError", obj);
        }
        if (emitter) {
          if (event === "error") {
            // Error will go unhandled if emitter hasn't been sent via callback.
            // Execute callback with the error instead.
            if (!sentEmitter) {
              return callback(obj);
            }
          }
          return emitter.emit(event, obj);
        }
    
        if (event === "error") {
          return callback(obj);
        }
    
        return callback(null, obj);
      }
    
      function messageCallback(msg) {
        if (timer) {
          clearTimeout(timer);
        }
    
        log.trace({ msg: msg ? msg.pojo : null }, "response received");
    
        if (expect === "abandon") {
          return sendResult("end", null);
        }
    
        if (msg instanceof ldap.SearchEntry) {
          return sendResult("searchEntry", msg);
        } else if (msg instanceof ldap.SearchReference) {
          return sendResult("searchReference", msg);
        } else {
          tracker.remove(message.messageId);
          // Potentially mark client as idle
          self._updateIdle();
    
          if (msg instanceof ldap.LDAPResult) {
            if (msg.status !== 0 && expect.indexOf(msg.status) === -1) {
              return sendResult("error", ldap.getError(msg));
            }
            return sendResult("end", msg);
          } else if (msg instanceof Error) {
            return sendResult("error", msg);
          } else {
            return sendResult("error", new ldap.ProtocolError(msg.type));
          }
        }
      }
    
      function onRequestTimeout() {
        self.emit("timeout", message);
        const { callback: cb } = tracker.fetch(message.messageId);
        if (cb) {
          // FIXME: the timed-out request should be abandoned
          cb(new errors.TimeoutError("request timeout (client interrupt)"));
        }
      }
    
      function writeCallback() {
        if (expect === "abandon") {
          // Mark the messageId specified as abandoned
          tracker.abandon(message.abandonId);
          // No need to track the abandon request itself
          tracker.remove(message.id);
          return callback(null);
        } else if (expect === "unbind") {
          conn.unbindMessageID = message.id;
          // Mark client as disconnected once unbind clears the socket
          self.connected = false;
          // Some servers will RST the connection after receiving an unbind.
          // Socket errors are blackholed since the connection is being closed.
          conn.removeAllListeners("error");
          conn.on("error", function () {});
          conn.end();
        } else if (emitter) {
          sentEmitter = true;
          callback(null, emitter);
          emitter.emit("searchRequest", message);
          return;
        }
        return false;
      }
    
      // Start actually doing something...
      tracker.track(message, messageCallback);
      // Mark client as active
      this._updateIdle(true);
    
      if (self.timeout) {
        log.trace("Setting timeout to %d", self.timeout);
        timer = setTimeout(onRequestTimeout, self.timeout);
      }
    
      log.trace("sending request %j", message.pojo);
    
      try {
        const messageBer = message.toBer();
        return conn.write(messageBer.buffer, writeCallback);
      } catch (e) {
        if (timer) {
          clearTimeout(timer);
        }
    
        log.trace({ err: e }, "Error writing message to socket");
        return callback(e);
      }
    };