All files / server/src/utils jwt.ts

88.57% Statements 31/35
78.57% Branches 11/14
100% Functions 7/7
88.57% Lines 31/35

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111              43x     43x 1440x         1440x       1440x     43x 3009x 3009x 2953x 2953x                         2953x                     977x               977x                 43x 421x 421x 421x 8x   419x     43x 4449x   1425x     2481x     543x             43x       1628x 496x 1464x     496x 518x         1603x    
import jsonwebtoken from "jsonwebtoken";
import { config } from "shared/config";
import { UserGroup } from "shared/types/models/user";
import { exhaustiveSwitchGuard } from "shared/utils/exhaustiveSwitchGuard";
import { JWTBody, JWTBodySchema, JWTResponse } from "server/types/jwtTypes";
import { logger } from "server/utils/logger";
 
const { TokenExpiredError } = jsonwebtoken;
type SignOptions = jsonwebtoken.SignOptions;
 
export const getJWT = (userGroup: UserGroup, username: string): string => {
  const payload = {
    username,
    userGroup,
  };
 
  const options: SignOptions = {
    expiresIn: "14 days",
  };
 
  return jsonwebtoken.sign(payload, getSecret(userGroup), options);
};
 
export const verifyJWT = (jwt: string, userGroup: UserGroup): JWTResponse => {
  try {
    const jwtBody = jsonwebtoken.verify(jwt, getSecret(userGroup));
    const result = JWTBodySchema.safeParse(jwtBody);
    Iif (!result.success) {
      // A valid signature with an invalid body means we created a bad JWT -
      // log it, unlike tampered tokens which are just noise
      logger.error(
        new Error("Error validating JWT body", { cause: result.error }),
      );
      return {
        status: "error",
        message: "Unknown JWT error",
        body: { username: "", userGroup: UserGroup.USER, iat: 0, exp: 0 },
      };
    }
 
    return {
      body: {
        username: result.data.username,
        userGroup: result.data.userGroup,
        iat: result.data.iat,
        exp: result.data.exp,
      },
      status: "success",
      message: "success",
    };
  } catch (error) {
    Iif (error instanceof TokenExpiredError) {
      return {
        status: "error",
        message: "Expired JWT",
        body: { username: "", userGroup: UserGroup.USER, iat: 0, exp: 0 },
      };
    }
 
    return {
      status: "error",
      message: "Unknown JWT error",
      body: { username: "", userGroup: UserGroup.USER, iat: 0, exp: 0 },
    };
  }
};
 
// Be careful: this does not verify JWT signature
export const decodeJWT = (jwt: string): JWTBody | null => {
  const decodedJwt = jsonwebtoken.decode(jwt);
  const result = JWTBodySchema.safeParse(decodedJwt);
  if (!result.success) {
    return null;
  }
  return result.data;
};
 
const getSecret = (userGroup: UserGroup): string => {
  switch (userGroup) {
    case UserGroup.ADMIN: {
      return config.server().jwtSecretKeyAdmin;
    }
    case UserGroup.USER: {
      return config.server().jwtSecretKey;
    }
    case UserGroup.HELPER: {
      return config.server().jwtSecretKeyHelp;
    }
    default:
      return exhaustiveSwitchGuard(userGroup);
  }
};
 
export const getJwtResponse = (
  jwt: string,
  requiredUserGroup: UserGroup | UserGroup[],
): JWTResponse => {
  if (Array.isArray(requiredUserGroup)) {
    const responses = requiredUserGroup.map((userGroup) => {
      return verifyJWT(jwt, userGroup);
    });
 
    return (
      responses.find((response) => response.status === "success") ??
      responses[0]
    );
  }
 
  return verifyJWT(jwt, requiredUserGroup);
};