user_redirect.js

/**
## /user/redirect

The module exports the default redirect method called from the default or a custom user authentication method.

@requires jsonwebtoken
@requires module:/user/acl

@module /user/redirect
*/

// biome-ignore assist/source/organizeImports: jsonwebtoken must be imported before the sign method is declared.
import jsonwebtoken from 'jsonwebtoken';
const { sign } = jsonwebtoken;
import acl from './acl.js';

/**
@function redirect

@description
A user object from the acl module is performed with the lookup property flag in the provided user param.

A user cookie is signed with the jsonwebtoken library and set on the response header.

The method checks for a redirect location on the user object or a `_redirect` cookie and sets the location header to the redirect location or the base directory if no redirect target is found.

The redirect cookie is destroyed [set to NULL] with the response header.

The response is sent with a 302 status code to redirect the client to the location header URL.

@param {req} req HTTP request.
@param {res} res HTTP response.
@param {object} user The user object should contain an email property and optionally a lookup property which will trigger a lookup in the ACL for the user email to assign any additional properties from the ACL to the user object before signing the cookie.
@property {string} user.email The email property is required to lookup the user in the ACL and assign any additional properties to the user object before signing the cookie.
@property {boolean} [user.lookup] The lookup property flag will trigger a lookup in the ACL for the user email to assign any additional properties to the user object before signing the cookie.
@property {string} [user.redirect] Redirect target provided by an authentication flow such as SAML RelayState.
*/
export default async function redirect(req, res, user) {
  if (user.lookup) {
    if (acl === null) {
      return res.status(405).send('ACL unavailable.');
    }

    const rows = await acl(
      `
      SELECT email, admin, language, roles, blocked, approved, verified
      FROM acl_schema.acl_table
      WHERE lower(email) = lower($1);`,
      [user.email],
    );

    if (rows instanceof Error) {
      res.setHeader(
        'Set-Cookie',
        `${xyzEnv.TITLE}=null; Max-Age=0; ${xyzEnv.COOKIE_PROPS}`,
      );
      return res.status(500).send('Failed to retrieve user from ACL');
    }

    if (!rows[0]) {
      return res.status(401).send('User not found.');
    }

    if (rows[0].blocked) {
      res.setHeader(
        'Set-Cookie',
        `${xyzEnv.TITLE}=null; Max-Age=0; ${xyzEnv.COOKIE_PROPS}`,
      );
      return res.status(403).send('User blocked in ACL.');
    }

    if (rows[0].verified === false) {
      return res.status(401).send('User not verified in ACL.');
    }

    if (rows[0].approved === false) {
      return res.status(401).send('User not approved in ACL.');
    }

    Object.assign(user, rows[0]);
  }

  const redirect = user.redirect || req.cookies?.[`${xyzEnv.TITLE}_redirect`];
  let token;
  //If the user is granted from external providers,
  //These properties may already exist.
  if (!user.exp)
    token = sign(user, xyzEnv.SECRET, {
      expiresIn: xyzEnv.COOKIE_TTL,
      algorithm: xyzEnv.SECRET_ALGORITHM,
    });

  token ??= sign(user, xyzEnv.SECRET, {
    algorithm: xyzEnv.SECRET_ALGORITHM,
  });

  const user_cookie = `${xyzEnv.TITLE}=${token}; Max-Age=${xyzEnv.COOKIE_TTL}; ${xyzEnv.COOKIE_PROPS}`;

  const redirect_cookie = `${xyzEnv.TITLE}_redirect=null; Max-Age=0; ${xyzEnv.COOKIE_PROPS}`;

  const location = redirectLocation(redirect);

  res.setHeader('Set-Cookie', [user_cookie, redirect_cookie]);
  res.setHeader('location', location);
  res.status(302).send();
}

function redirectLocation(redirect) {
  if (!redirect) return `${xyzEnv.DIR}/`;

  try {
    const location = decodeURIComponent(redirect).replaceAll(/[;\r\n]/g, '');

    if (location.startsWith('/') && !location.startsWith('//')) {
      return location;
    }
  } catch {
    // Fall through to the safe default for malformed cookie values.
  }

  return `${xyzEnv.DIR}/`;
}