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

95.83% Statements 23/24
93.75% Branches 15/16
100% Functions 5/5
95.83% Lines 23/24

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                2x           6x         6x       7x             7x 6x 6x       6x 6x 1x     5x             7x 6x 1x     5x                 4x 4x 4x 2x     2x 2x 1x     1x    
import { scrypt as scryptCallback, timingSafeEqual } from "node:crypto";
import { promisify } from "node:util";
 
import { getActiveClient, getExternalApiAuthConfig } from "./config";
import { CLIENT_CREDENTIALS_GRANT } from "./constants";
import { createExternalAccessToken, verifyExternalAccessToken } from "./jwt";
import { ExternalAccessTokenClaims, ExternalApiAuthConfig, ExternalApiClient } from "./types";
 
const scrypt = promisify(scryptCallback);
 
async function verifyClientSecret(
  client: ExternalApiClient,
  clientSecret: string,
): Promise<boolean> {
  const derived = (await scrypt(
    clientSecret,
    client.secretSalt,
    client.secretHash.length,
  )) as Buffer;
  return derived.length === client.secretHash.length && timingSafeEqual(derived, client.secretHash);
}
 
function clientSupportsClientCredentialsGrant(client: ExternalApiClient): boolean {
  return client.grants.includes(CLIENT_CREDENTIALS_GRANT);
}
 
export async function validateClientCredentials(
  clientId: string,
  clientSecret: string,
): Promise<{ config: ExternalApiAuthConfig; client: ExternalApiClient } | null> {
  const config = await getExternalApiAuthConfig();
  const client = getActiveClient(config, clientId);
  Iif (!client || !clientSupportsClientCredentialsGrant(client)) {
    return null;
  }
 
  const isValidSecret = await verifyClientSecret(client, clientSecret);
  if (!isValidSecret) {
    return null;
  }
 
  return { config, client };
}
 
export async function issueClientCredentialsAccessToken(
  clientId: string,
  clientSecret: string,
): Promise<{ accessToken: string; client: ExternalApiClient } | null> {
  const authResult = await validateClientCredentials(clientId, clientSecret);
  if (!authResult) {
    return null;
  }
 
  return {
    accessToken: createExternalAccessToken(authResult.config, authResult.client),
    client: authResult.client,
  };
}
 
export async function authorizeExternalAccessToken(
  token: string,
): Promise<{ claims: ExternalAccessTokenClaims; client: ExternalApiClient } | null> {
  const config = await getExternalApiAuthConfig();
  const claims = verifyExternalAccessToken(token, config);
  if (!claims) {
    return null;
  }
 
  const client = getActiveClient(config, claims.client_id);
  if (!client || !clientSupportsClientCredentialsGrant(client)) {
    return null;
  }
 
  return { claims, client };
}