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

77.58% Statements 45/58
73.46% Branches 36/49
100% Functions 14/14
77.19% Lines 44/57

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 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166                                2x     68x     68x       8x 8x       8x 8x     8x       26x 26x 26x       26x 26x     26x       13x       13x 13x           13x 13x                           13x 13x           13x       13x 13x 13x       13x                       8x 8x         8x       8x 8x 8x       8x     13x         12x   12x         3x     9x 8x 8x           8x             8x 9x         23x    
import { getSecret, validateEnvVariable } from "shared-utils";
 
import { SECRET_CACHE_TTL_MS } from "./constants";
import {
  ExternalAllowedLocation,
  ExternalApiAuthConfig,
  ExternalApiClient,
  ExternalClientStatus,
} from "./types";
 
type CachedConfig = {
  secretArn: string;
  loadedAtMs: number;
  config: ExternalApiAuthConfig;
};
 
let cachedConfig: CachedConfig | null = null;
 
function ensureNonEmptyString(value: unknown, fieldName: string): string {
  Iif (typeof value !== "string" || value.trim() === "") {
    throw new Error(`Invalid external auth config: ${fieldName} must be a non-empty string.`);
  }
  return value.trim();
}
 
function parseHex(value: unknown, fieldName: string): Buffer {
  const parsedValue = ensureNonEmptyString(value, fieldName);
  Iif (!/^[0-9a-f]+$/i.test(parsedValue) || parsedValue.length % 2 !== 0) {
    throw new Error(`Invalid external auth config: ${fieldName} must be a valid hex string.`);
  }
 
  const asBuffer = Buffer.from(parsedValue, "hex");
  Iif (asBuffer.length === 0) {
    throw new Error(`Invalid external auth config: ${fieldName} cannot be empty.`);
  }
  return asBuffer;
}
 
function parseBase64(value: unknown, fieldName: string): Buffer {
  const parsedValue = ensureNonEmptyString(value, fieldName);
  const normalized = parsedValue.replace(/-/g, "+").replace(/_/g, "/");
  Iif (!/^[A-Za-z0-9+/=]+$/.test(normalized)) {
    throw new Error(`Invalid external auth config: ${fieldName} must be base64 encoded.`);
  }
 
  const asBuffer = Buffer.from(normalized, "base64");
  Iif (asBuffer.length === 0) {
    throw new Error(`Invalid external auth config: ${fieldName} cannot decode to an empty value.`);
  }
  return asBuffer;
}
 
function parseAllowedLocations(rawAllowedLocations: unknown): ExternalAllowedLocation[] {
  Iif (!Array.isArray(rawAllowedLocations)) {
    throw new Error("Invalid external auth config: client.allowedLocations must be an array.");
  }
 
  return rawAllowedLocations.map((location, index) => {
    Iif (!location || typeof location !== "object") {
      throw new Error(
        `Invalid external auth config: client.allowedLocations[${index}] must be an object.`,
      );
    }
 
    const typedLocation = location as Record<string, unknown>;
    return {
      bucket: ensureNonEmptyString(
        typedLocation.bucket,
        `client.allowedLocations[${index}].bucket`,
      ),
      prefix:
        typeof typedLocation.prefix === "string" && typedLocation.prefix.trim().length > 0
          ? typedLocation.prefix
          : "",
    };
  });
}
 
function parseStatus(value: unknown): ExternalClientStatus {
  Eif (value === "ACTIVE" || value === "INACTIVE") {
    return value;
  }
  throw new Error("Invalid external auth config: client.status must be ACTIVE or INACTIVE.");
}
 
function parseClient(client: unknown, index: number): ExternalApiClient {
  Iif (!client || typeof client !== "object") {
    throw new Error(`Invalid external auth config: clients[${index}] must be an object.`);
  }
 
  const typedClient = client as Record<string, unknown>;
  const grants = typedClient.grants;
  Iif (!Array.isArray(grants) || grants.some((grant) => typeof grant !== "string")) {
    throw new Error(`Invalid external auth config: clients[${index}].grants must be string[].`);
  }
 
  return {
    clientId: ensureNonEmptyString(typedClient.clientId, `clients[${index}].clientId`),
    status: parseStatus(typedClient.status),
    grants: grants as string[],
    secretSalt: parseBase64(typedClient.secretSalt, `clients[${index}].secretSalt`),
    secretHash: parseBase64(typedClient.secretHash, `clients[${index}].secretHash`),
    allowedLocations: parseAllowedLocations(typedClient.allowedLocations),
  };
}
 
function parseConfig(configString: string): ExternalApiAuthConfig {
  let parsedConfig: unknown;
  try {
    parsedConfig = JSON.parse(configString);
  } catch {
    throw new Error("Invalid external auth config: secret value must be valid JSON.");
  }
 
  Iif (!parsedConfig || typeof parsedConfig !== "object") {
    throw new Error("Invalid external auth config: root value must be an object.");
  }
 
  const typedConfig = parsedConfig as Record<string, unknown>;
  const clients = typedConfig.clients;
  Iif (!Array.isArray(clients)) {
    throw new Error("Invalid external auth config: clients must be an array.");
  }
 
  return {
    issuer: ensureNonEmptyString(typedConfig.issuer, "issuer"),
    jwtSigningKey: parseHex(typedConfig.jwtSigningSecretHex, "jwtSigningSecretHex"),
    clients: clients.map((client, index) => parseClient(client, index)),
  };
}
 
export async function getExternalApiAuthConfig(): Promise<ExternalApiAuthConfig> {
  const secretArn = validateEnvVariable("externalApiAuthSecretArn");
 
  if (
    cachedConfig &&
    cachedConfig.secretArn === secretArn &&
    Date.now() - cachedConfig.loadedAtMs < SECRET_CACHE_TTL_MS
  ) {
    return cachedConfig.config;
  }
 
  const secretValue = await getSecret(secretArn);
  const config = parseConfig(secretValue);
  cachedConfig = {
    secretArn,
    loadedAtMs: Date.now(),
    config,
  };
 
  return config;
}
 
export function getActiveClient(
  config: ExternalApiAuthConfig,
  clientId: string,
): ExternalApiClient | undefined {
  return config.clients.find(
    (client) => client.clientId === clientId && client.status === "ACTIVE",
  );
}
 
export function resetExternalApiAuthConfigCache() {
  cachedConfig = null;
}