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 1859x         1859x       1859x     43x 3558x 3558x 3502x 3502x                         3502x                     1147x               1147x                 43x 502x 502x 502x 8x   500x     43x 5417x   1844x     2945x     628x             43x       1926x 581x 1719x     581x 603x         1901x    
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);
};