LibreChat icon indicating copy to clipboard operation
LibreChat copied to clipboard

Enhancement: support authorization via auth proxy

Open Jipok opened this issue 1 year ago • 11 comments

What features would you like to see added?

Option FORWARD_AUTH_HEADER

More details

There are various authorizing reverse proxies. Here are two examples: https://github.com/oauth2-proxy/oauth2-proxy https://github.com/Jipok/Jauth They take care of authentication, authorization, registration, etc. They are easy to support for the developer - all that is needed is to process the configured header(most often this is Remote-User or X-Forwarded-User) where the username is specified. They are also convenient for the user, since there is no need to remember/store extra login/password pairs, provide a single entry point for their own services and increase security.

I'm not good at coding in js, but I was able to implement a simple way that works for me: Just one change in: https://github.com/danny-avila/LibreChat/blob/dd8038b375901d52eb2d2db32ad43534909e90ba/api/server/controllers/AuthController.js#L72-L77

const refreshController = async (req, res) => {
  const refreshToken = req.headers.cookie ? cookies.parse(req.headers.cookie).refreshToken : null;
  
  // Handle Remote-User from auth proxy
  if (!refreshToken && process.env.FORWARD_AUTH_HEADER) {
    let forwardedUserName
    const headerName = process.env.FORWARD_AUTH_HEADER.toLowerCase()
    if (req.headers.hasOwnProperty(headerName)) {
      forwardedUserName = req.headers[headerName];
    } else {
      return res.status(500).send('FORWARD_AUTH_HEADER('+headerName+') not provided');
    }
    // If user doesn't exist, register them
    let user = await User.findOne({ username: forwardedUserName }, '_id').lean();
    if (!user) {
      //determine if this is the first registered user (not counting anonymous_user)
      const isFirstRegisteredUser = (await User.countDocuments({})) === 0;
      const newUser = await new User({
        provider: 'local',
        email: forwardedUserName + '@local.none',
        password: crypto.randomBytes(Math.ceil(10)).toString('hex'),
        name: forwardedUserName,
        username: forwardedUserName,
        avatar: null,
        role: isFirstRegisteredUser ? 'ADMIN' : 'USER',
      });
      const salt = bcrypt.genSaltSync(10);
      const hash = bcrypt.hashSync(newUser.password, salt);
      newUser.password = hash;
      await newUser.save();
    }
    user = await User.findOne({ username: forwardedUserName }, '_id').lean();
    const token = await setAuthTokens(user._id, res);
    return res.status(200).send({ token, user });
  }
  
  if (!refreshToken) {
    return res.status(200).send('Refresh token not provided');
  }
...

I understand that this code hardly corresponds to the complex architecture of the project. But it works for me and I hope someone can implement it correctly. The changes that really need to be made are resetting the jwt token if it is expired/incorrect so that the code can issue a new one automatically instead of showing the user the login page.

Documentation example: https://support.getgrist.com/install/forwarded-headers/#forwarded-headers

Which components are impacted by your request?

Endpoints

Pictures

https://github.com/danny-avila/LibreChat/assets/25588359/1fcf11f6-b302-4884-86ed-48efc3c80575

Code of Conduct

  • [X] I agree to follow this project's Code of Conduct

Jipok avatar Feb 21 '24 18:02 Jipok