diff --git a/.dev/compose.prodtest.yml b/.dev/compose.prodtest.yml index d0ac0611..a3cf73c2 100644 --- a/.dev/compose.prodtest.yml +++ b/.dev/compose.prodtest.yml @@ -20,7 +20,7 @@ services: transportLibrary__platformClientSecret: "test" transportLibrary__addressGenerationHostnameOverride: "localhost" DATABASE_CONNECTION_STRING: "mongodb://mongo:27017" - API_KEY: xxx + API_KEY: This_is_a_test_APIKEY_with_30_chars+ DEBUG: true depends_on: mongo: diff --git a/.dev/config.json b/.dev/config.json index 8685c05c..14064f8e 100644 --- a/.dev/config.json +++ b/.dev/config.json @@ -5,7 +5,7 @@ }, "infrastructure": { "httpServer": { - "apiKey": "xxx" + "apiKey": "This_is_a_test_APIKEY_with_30_chars+" } }, "modules": { diff --git a/.dev/scripts/establishRelationshipAndSpamMessages.ts b/.dev/scripts/establishRelationshipAndSpamMessages.ts index 8109325a..0f5e67dd 100644 --- a/.dev/scripts/establishRelationshipAndSpamMessages.ts +++ b/.dev/scripts/establishRelationshipAndSpamMessages.ts @@ -4,12 +4,12 @@ import { ConnectorClient, ConnectorRelationshipStatus } from "@nmshd/connector-s async function run() { const connector1 = ConnectorClient.create({ baseUrl: "http://localhost:3000", - apiKey: "xxx" + apiKey: "This_is_a_test_APIKEY_with_30_chars+" }); const connector2 = ConnectorClient.create({ baseUrl: "http://localhost:3001", - apiKey: "xxx" + apiKey: "This_is_a_test_APIKEY_with_30_chars+" }); const { connector1Address, connector2Address } = await establishOrReturnRelationship(connector1, connector2); diff --git a/.dev/scripts/syncConnector1.sh b/.dev/scripts/syncConnector1.sh index ec9172aa..10144154 100755 --- a/.dev/scripts/syncConnector1.sh +++ b/.dev/scripts/syncConnector1.sh @@ -2,4 +2,4 @@ set -e -curl -H "X-API-KEY: xxx" -X POST http://localhost:3000/api/v2/Account/Sync +curl -H "X-API-KEY: This_is_a_test_APIKEY_with_30_chars+" -X POST http://localhost:3000/api/v2/Account/Sync diff --git a/.dev/scripts/syncConnector2.sh b/.dev/scripts/syncConnector2.sh index 19f3933a..9900a61a 100755 --- a/.dev/scripts/syncConnector2.sh +++ b/.dev/scripts/syncConnector2.sh @@ -2,4 +2,4 @@ set -e -curl -H "X-API-KEY: xxx" -X POST http://localhost:3001/api/v2/Account/Sync +curl -H "X-API-KEY: This_is_a_test_APIKEY_with_30_chars+" -X POST http://localhost:3001/api/v2/Account/Sync diff --git a/README_dev.md b/README_dev.md index 68ccfb33..c9ee02c8 100644 --- a/README_dev.md +++ b/README_dev.md @@ -143,18 +143,18 @@ npm run test:local -- testSuiteName { "debug": true, "transportLibrary": { - "baseUrl": "...", - "platformClientId": "...", - "platformClientSecret": "..." + "baseUrl": "", + "platformClientId": "", + "platformClientSecret": "" }, "database": { "driver": "lokijs", "folder": "./" }, "logging": { "categories": { "default": { "appenders": ["console"] } } }, - "infrastructure": { "httpServer": { "apiKey": "xxx", "port": 8080 } }, + "infrastructure": { "httpServer": { "apiKey": "", "port": 8080 } }, "modules": { "coreHttpApi": { "docs": { "enabled": true } } } } ``` -6. replace ... in the config with real values +6. replace the placeholders in the config with real values 7. start the connector using `CUSTOM_CONFIG_LOCATION=./local.config.json node dist/index.js start` It's now possible to access the connector on port 8080. Validating this is possible by accessing `http://localhost:8080/docs/swagger` in the browser. diff --git a/src/infrastructure/httpServer/HttpServer.ts b/src/infrastructure/httpServer/HttpServer.ts index fa9c83db..469ed938 100644 --- a/src/infrastructure/httpServer/HttpServer.ts +++ b/src/infrastructure/httpServer/HttpServer.ts @@ -91,21 +91,7 @@ export class HttpServer extends ConnectorInfrastructure this.useUnsecuredCustomEndpoints(); this.useHealthEndpoint(); - - if (this.configuration.apiKey) { - this.app.use(async (req, res, next) => { - const apiKeyFromHeader = req.headers["x-api-key"]; - if (!apiKeyFromHeader || apiKeyFromHeader !== this.configuration.apiKey) { - await sleep(1000); - res.status(401).send(Envelope.error(HttpErrors.unauthorized(), this.connectorMode)); - return; - } - - next(); - }); - } else { - this.logger.warn("No api key set in config, this Connector runs without any authentication! This is strongly discouraged."); - } + this.useApiKey(); this.useVersionEndpoint(); this.useResponsesEndpoint(); @@ -203,6 +189,36 @@ export class HttpServer extends ConnectorInfrastructure this.app.use(genericErrorHandler(this.connectorMode)); } + private useApiKey() { + if (!this.configuration.apiKey) { + switch (this.connectorMode) { + case "debug": + return; + case "production": + throw new Error("No API key set in configuration. This is required in production mode."); + } + } + + const apiKeyPolicy = /^(?=.*[A-Z].*[A-Z])(?=.*[!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~])(?=.*[0-9].*[0-9])(?=.*[a-z].*[a-z]).{30,}$/; + if (!this.configuration.apiKey.match(apiKeyPolicy)) { + this.logger.warn( + "The configured API key does not meet the requirements. It must be at least 30 characters long and contain at least 2 digits, 2 uppercase letters, 2 lowercase letters and 1 special character (!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~])." + ); + this.logger.warn("The API key will be used as is, but it is recommended to change it as it will not be supported in future versions."); + } + + this.app.use(async (req, res, next) => { + const apiKeyFromHeader = req.headers["x-api-key"]; + if (!apiKeyFromHeader || apiKeyFromHeader !== this.configuration.apiKey) { + await sleep(1000 * (Math.floor(Math.random() * 4) + 1)); + res.status(401).send(Envelope.error(HttpErrors.unauthorized(), this.connectorMode)); + return; + } + + next(); + }); + } + private useHealthEndpoint() { this.app.get("/health", async (_req: any, res: any) => { const health = await this.runtime.getHealth(); diff --git a/test/errors.test.ts b/test/errors.test.ts index 74b36a76..c73ea5b2 100644 --- a/test/errors.test.ts +++ b/test/errors.test.ts @@ -10,7 +10,9 @@ beforeAll(async () => { const baseUrl = await launcher.launchSimple(); axiosClient = axios.create({ baseURL: baseUrl, - validateStatus: (_) => true + validateStatus: (_) => true, + // eslint-disable-next-line @typescript-eslint/naming-convention + headers: { "X-API-KEY": launcher.apiKey } }); }, getTimeout(30000)); @@ -18,37 +20,28 @@ afterAll(() => launcher.stop()); describe("Errors", () => { test("http error 401", async () => { - const response = await axiosClient.get("/api/v2/Files"); + const response = await axiosClient.get("/api/v2/Files", { + // eslint-disable-next-line @typescript-eslint/naming-convention + headers: { "X-API-KEY": "invalid" } + }); expect(response.status).toBe(401); validateSchema(ValidationSchema.Error, response.data.error); }); test("http error 404", async () => { - const response = await axiosClient.get("/apii/v2/Files", { - headers: { - "X-API-KEY": "xxx" // eslint-disable-line @typescript-eslint/naming-convention - } - }); + const response = await axiosClient.get("/apii/v2/Files"); expect(response.status).toBe(404); validateSchema(ValidationSchema.Error, response.data.error); }); test("http error 405", async () => { - const response = await axiosClient.patch("/api/v2/Files", undefined, { - headers: { - "X-API-KEY": "xxx" // eslint-disable-line @typescript-eslint/naming-convention - } - }); + const response = await axiosClient.patch("/api/v2/Files", undefined); expect(response.status).toBe(405); validateSchema(ValidationSchema.Error, response.data.error); }); test("http error 400", async () => { - const response = await axiosClient.post("/api/v2/Files/Own", undefined, { - headers: { - "X-API-KEY": "xxx" // eslint-disable-line @typescript-eslint/naming-convention - } - }); + const response = await axiosClient.post("/api/v2/Files/Own", undefined); expect(response.status).toBe(400); expect(response.data.error.docs).toBe("https://enmeshed.eu/integrate/error-codes#error.runtime.validation.invalidPropertyValue"); validateSchema(ValidationSchema.Error, response.data.error); diff --git a/test/lib/Launcher.ts b/test/lib/Launcher.ts index 7e27e047..15fc1bda 100644 --- a/test/lib/Launcher.ts +++ b/test/lib/Launcher.ts @@ -20,7 +20,7 @@ export type ConnectorClientWithMetadata = ConnectorClient & { export class Launcher { private readonly _processes: { connector: ChildProcess; webhookServer: Server | undefined }[] = []; - private readonly apiKey = "xxx"; + public readonly apiKey = "This_is_a_test_APIKEY_with_30_chars+"; public async launchSimple(): Promise { const port = await getPort();