diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c6cb160..449832d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# [4.1.0](https://github.com/jagregory/cognito-local/compare/v4.0.0...v4.1.0) (2025-02-16) + + +### Features + +* **api:** support https ([c23eefe](https://github.com/jagregory/cognito-local/commit/c23eefec0becf3423ab618414039022526b59c5e)) + # [4.0.0](https://github.com/jagregory/cognito-local/compare/v3.23.3...v4.0.0) (2025-02-11) diff --git a/README.md b/README.md index 127f06ce..d7795978 100644 --- a/README.md +++ b/README.md @@ -309,13 +309,20 @@ Before starting Cognito Local, create a config file if one doesn't already exist You can edit that `.cognito/config.json` and add any of the following settings: | Setting | Type | Default | Description | -| ------------------------------------------ | ---------- | ----------------------- | ----------------------------------------------------------- | +|--------------------------------------------|------------|-------------------------|-------------------------------------------------------------| | `LambdaClient` | `object` | | Any setting you would pass to the AWS.Lambda Node.js client | | `LambdaClient.credentials.accessKeyId` | `string` | `local` | | | `LambdaClient.credentials.secretAccessKey` | `string` | `local` | | | `LambdaClient.endpoint` | `string` | `local` | | | `LambdaClient.region` | `string` | `local` | | | `TokenConfig.IssuerDomain` | `string` | `http://localhost:9229` | Issuer domain override | +| `ServerConfig` | `object` | | Any setting you would pass to the Express server | +| `ServerConfig.hostname` | `string` | `localhost` | Hostname to listen on | +| `ServerConfig.port` | `number` | `9229` | Port to listen on | +| `ServerConfig.https` | `boolean` | `false` | Enable HTTPS | +| `ServerConfig.ca` | `string` | | Path to the TLS CA file (ServerConfig.https must be true) | +| `ServerConfig.cert` | `string` | | Path to the TLS Cert file (ServerConfig.https must be true) | +| `ServerConfig.key` | `string` | | Path to the TLS Key file (ServerConfig.https must be true) | | `TriggerFunctions` | `object` | `{}` | Trigger name to Function name mapping | | `TriggerFunctions.CustomMessage` | `string` | | CustomMessage local lambda function name | | `TriggerFunctions.PostAuthentication` | `string` | | PostAuthentication local lambda function name | diff --git a/integration-tests/aws-sdk/setup.ts b/integration-tests/aws-sdk/setup.ts index b4cd4489..7437b5f5 100644 --- a/integration-tests/aws-sdk/setup.ts +++ b/integration-tests/aws-sdk/setup.ts @@ -81,11 +81,13 @@ export const withCognitoSdk = DefaultConfig.TokenConfig ), }); - const server = createServer(router, ctx.logger); - httpServer = await server.start({ + const server = createServer(router, ctx.logger, { + development: false, hostname: "127.0.0.1", + https: false, port: 0, }); + httpServer = await server.start(); const address = httpServer.address(); if (!address) { diff --git a/integration-tests/server.test.ts b/integration-tests/server.test.ts index cd954988..f4c0c808 100644 --- a/integration-tests/server.test.ts +++ b/integration-tests/server.test.ts @@ -14,7 +14,7 @@ describe("HTTP server", () => { describe("/", () => { it("errors with missing x-azm-target header", async () => { const router = jest.fn(); - const server = createServer(router, MockLogger as any); + const server = createServer(router, MockLogger as any, {}); const response = await supertest(server.application).post("/"); @@ -24,7 +24,7 @@ describe("HTTP server", () => { it("errors with an poorly formatted x-azm-target header", async () => { const router = jest.fn(); - const server = createServer(router, MockLogger as any); + const server = createServer(router, MockLogger as any, {}); const response = await supertest(server.application) .post("/") @@ -43,7 +43,7 @@ describe("HTTP server", () => { }); const router = (target: string) => target === "valid" ? route : () => Promise.reject(); - const server = createServer(router, MockLogger as any); + const server = createServer(router, MockLogger as any, {}); const response = await supertest(server.application) .post("/") @@ -61,7 +61,7 @@ describe("HTTP server", () => { .mockRejectedValue(new UnsupportedError("integration test")); const router = (target: string) => target === "valid" ? route : () => Promise.reject(); - const server = createServer(router, MockLogger as any); + const server = createServer(router, MockLogger as any, {}); const response = await supertest(server.application) .post("/") @@ -87,7 +87,7 @@ describe("HTTP server", () => { const route = jest.fn().mockRejectedValue(error); const router = (target: string) => target === "valid" ? route : () => Promise.reject(); - const server = createServer(router, MockLogger as any); + const server = createServer(router, MockLogger as any, {}); const response = await supertest(server.application) .post("/") @@ -105,7 +105,7 @@ describe("HTTP server", () => { describe("jwks endpoint", () => { it("responds with our public key", async () => { - const server = createServer(jest.fn(), MockLogger as any); + const server = createServer(jest.fn(), MockLogger as any, {}); const response = await supertest(server.application).get( "/any-user-pool/.well-known/jwks.json" @@ -129,7 +129,7 @@ describe("HTTP server", () => { describe("OpenId Configuration Endpoint", () => { it("responds with open id configuration", async () => { - const server = createServer(jest.fn(), MockLogger as any); + const server = createServer(jest.fn(), MockLogger as any, {}); const response = await supertest(server.application).get( "/any-user-pool/.well-known/openid-configuration" diff --git a/src/bin/start.ts b/src/bin/start.ts index 8547af88..a97bd318 100644 --- a/src/bin/start.ts +++ b/src/bin/start.ts @@ -3,6 +3,7 @@ import { createDefaultServer } from "../server"; import Pino from "pino"; import PinoPretty from "pino-pretty"; +import * as https from "https"; const logger = Pino( { @@ -18,13 +19,9 @@ const logger = Pino( ); createDefaultServer(logger) + .then((server) => server.start()) .then((server) => { - const hostname = process.env.HOST ?? "localhost"; - const port = parseInt(process.env.PORT ?? "9229", 10); - return server.start({ hostname, port }); - }) - .then((httpServer) => { - const address = httpServer.address(); + const address = server.address(); if (!address) { throw new Error("Server started without address"); } @@ -33,7 +30,9 @@ createDefaultServer(logger) ? address : `${address.address}:${address.port}`; - logger.info(`Cognito Local running on http://${url}`); + const proto = server instanceof https.Server ? "https" : "http"; + + logger.info(`Cognito Local running on ${proto}://${url}`); }) .catch((err) => { logger.error(err); diff --git a/src/server/config.ts b/src/server/config.ts index ac9a5b18..a15b2061 100644 --- a/src/server/config.ts +++ b/src/server/config.ts @@ -5,6 +5,7 @@ import { UserPool } from "../services/userPoolService"; import { TokenConfig } from "../services/tokenGenerator"; import mergeWith from "lodash.mergewith"; import { KMSConfig } from "../services/crypto"; +import type { ServerOptions } from "./server"; export type UserPoolDefaults = Omit< UserPool, @@ -16,9 +17,13 @@ export interface Config { TriggerFunctions: FunctionConfig; UserPoolDefaults: UserPoolDefaults; KMSConfig?: AWS.KMS.ClientConfiguration & KMSConfig; + ServerConfig: ServerOptions; TokenConfig: TokenConfig; } +const port = parseInt(process.env.PORT ?? "9229", 10); +const hostname = process.env.HOST ?? "localhost"; + export const DefaultConfig: Config = { LambdaClient: { credentials: { @@ -32,8 +37,7 @@ export const DefaultConfig: Config = { UsernameAttributes: ["email"], }, TokenConfig: { - // TODO: this needs to match the actual host/port we started the server on - IssuerDomain: "http://localhost:9229", + IssuerDomain: `http://${hostname}:${port}`, }, KMSConfig: { credentials: { @@ -42,6 +46,12 @@ export const DefaultConfig: Config = { }, region: "local", }, + ServerConfig: { + port, + hostname, + development: !!process.env.COGNITO_LOCAL_DEVMODE, + https: false, + }, }; export const loadConfig = async ( diff --git a/src/server/defaults.ts b/src/server/defaults.ts index 2e0baf65..a2bb3629 100644 --- a/src/server/defaults.ts +++ b/src/server/defaults.ts @@ -76,8 +76,6 @@ export const createDefaultServer = async ( triggers, }), logger, - { - development: !!process.env.COGNITO_LOCAL_DEVMODE, - } + config.ServerConfig ); }; diff --git a/src/server/server.ts b/src/server/server.ts index ecc98580..1a1bf8ee 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -2,28 +2,33 @@ import bodyParser from "body-parser"; import cors from "cors"; import express from "express"; import * as http from "http"; +import * as https from "https"; import type { Logger } from "pino"; import * as uuid from "uuid"; import { CognitoError, UnsupportedError } from "../errors"; import { Router } from "./Router"; import PublicKey from "../keys/cognitoLocal.public.json"; import Pino from "pino-http"; +import { readFileSync } from "fs"; -export interface ServerOptions { - port: number; - hostname: string; - development: boolean; -} +export type ServerOptions = { + port?: number; + hostname?: string; + development?: boolean; +} & ( + | { https: true; key?: string; ca?: string; cert?: string } + | { https?: false } +); export interface Server { application: any; // eslint-disable-line - start(options?: Partial): Promise; + start(): Promise; } export const createServer = ( router: Router, logger: Logger, - options: Partial = {} + options: ServerOptions ): Server => { const pino = Pino({ logger, @@ -137,22 +142,28 @@ export const createServer = ( return { application: app, - start(startOptions) { - const actualOptions: ServerOptions = { - port: options?.port ?? 9229, - hostname: options?.hostname ?? "localhost", - development: options?.development ?? false, - ...options, - ...startOptions, - }; - - return new Promise((resolve, reject) => { - const httpServer = app.listen( - actualOptions.port, - actualOptions.hostname, - () => resolve(httpServer) - ); - httpServer.on("error", reject); + start() { + const hostname = options.hostname; + const port = options.port; + + return new Promise((resolve, reject) => { + const server = options.https + ? https.createServer( + { + ca: options.ca ? readFileSync(options.ca, "utf-8") : undefined, + cert: options.cert + ? readFileSync(options.cert, "utf-8") + : undefined, + key: options.key + ? readFileSync(options.key, "utf-8") + : undefined, + }, + app + ) + : http.createServer(app); + + server.listen(port, hostname, () => resolve(server)); + server.on("error", reject); }); }, };