All files / lib/lambda/external-auth jwt.ts

78.04% Statements 32/41
78.12% Branches 25/32
100% Functions 5/5
78.04% Lines 32/41

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 112 113 114 115 116 117 118 119 120 121                      18x       7x 7x                   6x 6x                     6x         6x 6x 6x 6x   6x             4x 4x       4x 4x 4x       4x         4x 4x         4x       4x 1x     3x 3x       3x               3x           3x         3x       3x 3x 1x     2x    
import { createHmac, timingSafeEqual } from "node:crypto";
 
import { CLIENT_CREDENTIALS_GRANT, TOKEN_EXPIRATION_SECONDS } from "./constants";
import { ExternalAccessTokenClaims, ExternalApiAuthConfig, ExternalApiClient } from "./types";
 
type JwtHeader = {
  alg: "HS256";
  typ: "JWT";
};
 
function toBase64Url(value: Buffer | string): string {
  return Buffer.from(value).toString("base64url");
}
 
function parseJsonBase64Url<T>(value: string): T | null {
  try {
    return JSON.parse(Buffer.from(value, "base64url").toString("utf8")) as T;
  } catch {
    return null;
  }
}
 
export function createExternalAccessToken(
  config: ExternalApiAuthConfig,
  client: ExternalApiClient,
): string {
  const now = Math.floor(Date.now() / 1000);
  const payload: ExternalAccessTokenClaims = {
    iss: config.issuer,
    sub: client.clientId,
    client_id: client.clientId,
    grant_type: CLIENT_CREDENTIALS_GRANT,
    grants: client.grants,
    token_use: "access",
    iat: now,
    exp: now + TOKEN_EXPIRATION_SECONDS,
  };
 
  const header: JwtHeader = {
    alg: "HS256",
    typ: "JWT",
  };
 
  const encodedHeader = toBase64Url(JSON.stringify(header));
  const encodedPayload = toBase64Url(JSON.stringify(payload));
  const signingInput = `${encodedHeader}.${encodedPayload}`;
  const signature = createHmac("sha256", config.jwtSigningKey).update(signingInput).digest();
 
  return `${signingInput}.${toBase64Url(signature)}`;
}
 
export function verifyExternalAccessToken(
  token: string,
  config: ExternalApiAuthConfig,
): ExternalAccessTokenClaims | null {
  const parts = token.split(".");
  Iif (parts.length !== 3) {
    return null;
  }
 
  const [encodedHeader, encodedPayload, encodedSignature] = parts;
  const header = parseJsonBase64Url<JwtHeader>(encodedHeader);
  Iif (!header || header.alg !== "HS256" || header.typ !== "JWT") {
    return null;
  }
 
  const expectedSignature = createHmac("sha256", config.jwtSigningKey)
    .update(`${encodedHeader}.${encodedPayload}`)
    .digest();
 
  let providedSignature: Buffer;
  try {
    providedSignature = Buffer.from(encodedSignature, "base64url");
  } catch {
    return null;
  }
 
  Iif (expectedSignature.length !== providedSignature.length) {
    return null;
  }
 
  if (!timingSafeEqual(expectedSignature, providedSignature)) {
    return null;
  }
 
  const payload = parseJsonBase64Url<ExternalAccessTokenClaims>(encodedPayload);
  Iif (!payload) {
    return null;
  }
 
  Iif (
    payload.iss !== config.issuer ||
    payload.grant_type !== CLIENT_CREDENTIALS_GRANT ||
    payload.token_use !== "access"
  ) {
    return null;
  }
 
  Iif (
    typeof payload.client_id !== "string" ||
    payload.client_id.trim() === "" ||
    typeof payload.sub !== "string" ||
    payload.sub.trim() === "" ||
    !Array.isArray(payload.grants) ||
    payload.grants.some((grant) => typeof grant !== "string")
  ) {
    return null;
  }
 
  Iif (typeof payload.exp !== "number" || typeof payload.iat !== "number") {
    return null;
  }
 
  const now = Math.floor(Date.now() / 1000);
  if (payload.exp <= now) {
    return null;
  }
 
  return payload;
}