All files / lib/lambda externalToken.ts

88.23% Statements 30/34
72.72% Branches 24/33
100% Functions 4/4
88.23% Lines 30/34

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                                  5x                   7x 7x         7x 5x 5x   1x   2x 1x   1x             5x 5x 5x   5x       5x 1x     4x       4x       4x                   7x     1x 7x 7x 3x     4x 4x       3x 1x     2x                 1x      
import { APIGatewayProxyEvent } from "aws-lambda";
import { response } from "libs/handler-lib";
 
import { TOKEN_EXPIRATION_SECONDS } from "./external-auth";
import { issueClientCredentialsAccessToken } from "./external-auth/service";
 
type TokenRequest = {
  grantType: string;
  clientId: string;
  clientCredential: string;
};
 
function errorResponse(
  statusCode: number,
  errorCode: "invalid_request" | "unsupported_grant_type" | "invalid_client" | "server_error",
  description: string,
) {
  return response({
    statusCode,
    body: {
      error: errorCode,
      error_description: description,
    },
  });
}
 
function parseBody(event: APIGatewayProxyEvent): TokenRequest | ReturnType<typeof errorResponse> {
  const contentType = event.headers["Content-Type"] || event.headers["content-type"] || "";
  Iif (!event.body) {
    return errorResponse(400, "invalid_request", "Request body is required.");
  }
 
  let body: Record<string, unknown>;
  if (contentType.includes("application/json")) {
    try {
      body = JSON.parse(event.body);
    } catch {
      return errorResponse(400, "invalid_request", "Invalid JSON payload.");
    }
  } else if (contentType.includes("application/x-www-form-urlencoded")) {
    body = Object.fromEntries(new URLSearchParams(event.body).entries());
  } else {
    return errorResponse(
      400,
      "invalid_request",
      "Content-Type must be application/json or application/x-www-form-urlencoded.",
    );
  }
 
  const grantType = body.grant_type ?? body.grantType;
  const clientId = body.client_id ?? body.clientId;
  const clientCredential = body.client_secret ?? body.clientSecret;
 
  Iif (typeof grantType !== "string" || grantType.trim() === "") {
    return errorResponse(400, "invalid_request", "grant_type is required.");
  }
 
  if (grantType !== "client_credentials") {
    return errorResponse(400, "unsupported_grant_type", "Only client_credentials is supported.");
  }
 
  Iif (typeof clientId !== "string" || clientId.trim() === "") {
    return errorResponse(400, "invalid_request", "client_id is required.");
  }
 
  Iif (typeof clientCredential !== "string" || clientCredential.trim() === "") {
    return errorResponse(400, "invalid_request", "client_secret is required."); // pragma: allowlist secret
  }
 
  return {
    grantType,
    clientId: clientId.trim(),
    clientCredential,
  };
}
 
function isParsedBodyResponse(
  value: TokenRequest | ReturnType<typeof errorResponse>,
): value is ReturnType<typeof errorResponse> {
  return (value as ReturnType<typeof errorResponse>).statusCode !== undefined;
}
 
export const handler = async (event: APIGatewayProxyEvent) => {
  const parsedBody = parseBody(event);
  if (isParsedBodyResponse(parsedBody)) {
    return parsedBody;
  }
 
  try {
    const tokenResult = await issueClientCredentialsAccessToken(
      parsedBody.clientId,
      parsedBody.clientCredential,
    );
    if (!tokenResult) {
      return errorResponse(401, "invalid_client", "Invalid client credentials.");
    }
 
    return response({
      statusCode: 200,
      body: {
        access_token: tokenResult.accessToken,
        token_type: "Bearer",
        expires_in: TOKEN_EXPIRATION_SECONDS,
      },
    });
  } catch {
    return errorResponse(500, "server_error", "Internal server error.");
  }
};