[ Web Proxy ]
URL:
Viewing: https://raw.githubusercontent.com/devforth/adminforth/bugfix/unsuitable-type/adminforth/auth.ts [Back]  [Original]

import jwt from 'jsonwebtoken';
import crypto from 'crypto';
import AdminForth from './index.js';
import { IAdminForthAuth } from './types/Back.js';

// Function to generate a password hash using PBKDF2
function calcPasswordHash(password, salt, iterations = 100000, keyLength = 64, digest = 'sha512') {
  return new Promise((resolve, reject) => {
    crypto.pbkdf2(password, salt, iterations, keyLength, digest, (err, derivedKey) => {
      if (err) reject(err);
      resolve(derivedKey.toString('hex'));
    });
  });
}

// Function to generate a random salt
function generateSalt(length = 16) {
  return crypto.randomBytes(length).toString('hex');
}

function parseTimeToSeconds(time: string): number {
  const unit = time.slice(-1);
  const value = parseInt(time.slice(0, -1), 10);
  switch (unit) {
    case 's':
      return value;
    case 'm':
      return value * 60;
    case 'h':
      return value * 60 * 60;
    case 'd':
      return value * 60 * 60 * 24;
    default:
      throw new Error(`Invalid time unit: ${unit}`);
  }
}

class AdminForthAuth implements IAdminForthAuth {
  adminforth: AdminForth;

  constructor(adminforth) {
    this.adminforth = adminforth;
  }

  getClientIp(headers: object) {
    const clientIpHeader = this.adminforth.config.auth.clientIpHeader;

    const headersLower = Object.keys(headers).reduce((acc, key) => {
      acc[key.toLowerCase()] = headers[key];
      return acc;
    }, {});
    if (clientIpHeader) {
      return headersLower[clientIpHeader.toLowerCase()] || 'unknown';
    } else {
      // first try common headers which can't bee spoofed, in other words
      // most common to nginx/traefik/apache
      // then fallback to less secure headers
      return headersLower['x-forwarded-for']?.split(',').shift().trim() ||
       headersLower['x-real-ip'] || 
       headersLower['x-client-ip'] || 
       headersLower['x-cluster-client-ip'] || 
       headersLower['forwarded'] || 
       headersLower['remote-addr'] || 
       headersLower['client-ip'] || 
       headersLower['client-address'] || 
       headersLower['client'] || 
       headersLower['x-host'] || 
       headersLower['host'] || 
       'unknown';
    }
  }

  removeAuthCookie(response) {
    const brandSlug = this.adminforth.config.customization.brandNameSlug;
    response.setHeader('Set-Cookie', `adminforth_${brandSlug}_jwt=; Path=${this.adminforth.config.baseUrl || '/'}; HttpOnly; SameSite=Strict; Expires=Thu, 01 Jan 1970 00:00:00 GMT`);
  }

  setAuthCookie({ expireInDays, response, username, pk}: {
    expireInDays?: number,
    response: any, 
    username: string, 
    pk: string | null
  }) {
    const expiresIn: string = expireInDays ? `${expireInDays}d` : (process.env.ADMINFORTH_AUTH_EXPIRESIN || '24h');
    // might be h,m,d in string
    const expiresInSec = parseTimeToSeconds(expiresIn);

    const token = this.issueJWT({ username, pk}, 'auth', expiresIn);
    const expiresCookieFormat = new Date(Date.now() + expiresInSec * 1000).toUTCString();
    
    const brandSlug = this.adminforth.config.customization.brandNameSlug;
    response.setHeader('Set-Cookie', `adminforth_${brandSlug}_jwt=${token}; Path=${this.adminforth.config.baseUrl || '/'}; HttpOnly; SameSite=Strict; Expires=${expiresCookieFormat}`);
  }

  removeCustomCookie({response, name}) {
    const brandSlug = this.adminforth.config.customization.brandNameSlug;
    response.setHeader('Set-Cookie', `adminforth_${brandSlug}_${name}=; Path=${this.adminforth.config.baseUrl || '/'}; HttpOnly; SameSite=Strict; Expires=Thu, 01 Jan 1970 00:00:00 GMT`);
  }

  setCustomCookie({ response, payload }: {
    response: any, payload: {name: string, value: string, expiry: number, httpOnly: boolean}
  }) {
    const {name, value, expiry, httpOnly} = payload;
    const brandSlug = this.adminforth.config.customization.brandNameSlug;
    response.setHeader('Set-Cookie', `adminforth_${brandSlug}_${name}=${value}; Path=${this.adminforth.config.baseUrl || '/'};${
      httpOnly ? ' HttpOnly;' : ''
    } SameSite=Strict; Expires=${new Date(Date.now() + expiry).toUTCString() } `);
  }
 
  issueJWT(payload: Object, type: string, expiresIn: string = '24h'): string {
    // read ADMINFORH_SECRET from environment if not drop error
    const secret = process.env.ADMINFORTH_SECRET;
    if (!secret) {
      throw new Error('ADMINFORTH_SECRET environment not set');
    }

    // issue JWT token
    return jwt.sign({...payload, t: type}, secret, { expiresIn });
  }

  async verify(jwtToken: string, mustHaveType: string, decodeUser: boolean | undefined = true): Promise {
    // read ADMINFORH_SECRET from environment if not drop error
    const secret = process.env.ADMINFORTH_SECRET;
    if (!secret) {
      throw new Error('ADMINFORTH_SECRET environment not set');
    }
    let decoded;
    try {
      // verify JWT token
      decoded = jwt.verify(jwtToken, secret);
    } catch (err) {
      if (err.name === 'TokenExpiredError') {
        console.error('Token expired:', err.message);
      } else  if (err.name === 'JsonWebTokenError') {
        console.error('Token error:', err.message);
      } else {
        console.error('Failed to verify JWT token', err);
      }
      return null;
    }
    const { pk, t } = decoded;
    if (t !== mustHaveType) {
      console.error(`Invalid token type during verification: ${t}, must be ${mustHaveType}`);
      return null;
    }
    if (decodeUser !== false) {
      const dbUser = await this.adminforth.getUserByPk(pk);
      if (!dbUser) {
        console.error(`User with pk ${pk} not found in database`);
        // will logout user which was deleted
        return null;
      }
      decoded.dbUser = dbUser;
    }
    
    return decoded;
  }

  static async generatePasswordHash(password) {
    const salt = generateSalt();
    const hashedPassword = await calcPasswordHash(password, salt);
    return `${salt}:${hashedPassword}`;
  }

  static async verifyPassword(password, hashedPassword) {
    const [salt, hash] = hashedPassword.split(':');
    const newHash = await calcPasswordHash(password, salt);
    return newHash === hash;
  }


}

export default AdminForthAuth;

Web Proxy Viewer  |  New URL  |  Original Page