From f2133176b57189a0840e04ec222d774568f5a8f0 Mon Sep 17 00:00:00 2001 From: Gerald Baulig Date: Fri, 28 Nov 2025 13:02:16 +0100 Subject: [PATCH 1/3] fix(kafka-client): ensure bigint at all cost --- packages/kafka-client/src/events/provider/kafka/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kafka-client/src/events/provider/kafka/index.ts b/packages/kafka-client/src/events/provider/kafka/index.ts index 1a76eb81..4a82fcc9 100644 --- a/packages/kafka-client/src/events/provider/kafka/index.ts +++ b/packages/kafka-client/src/events/provider/kafka/index.ts @@ -323,7 +323,7 @@ export class Topic { offsets: [{ topic: this.name, partition: 0, - offset: offsetValue + offset: BigInt(offsetValue) }], }).then(stream => { this.provider.logger.info(`Consumer for topic '${this.name}' subscribed`); From 2d85d41f6b980f80847fbc3b1d597f1d979e7730 Mon Sep 17 00:00:00 2001 From: Gerald Baulig Date: Fri, 28 Nov 2025 14:40:14 +0100 Subject: [PATCH 2/3] fix(resource-base-interface): experimental WorkerBase init topics sequential --- .../src/experimental/WorkerBase.ts | 30 +++++++++---------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/packages/resource-base-interface/src/experimental/WorkerBase.ts b/packages/resource-base-interface/src/experimental/WorkerBase.ts index 3e34793b..315b87ad 100644 --- a/packages/resource-base-interface/src/experimental/WorkerBase.ts +++ b/packages/resource-base-interface/src/experimental/WorkerBase.ts @@ -342,27 +342,25 @@ export abstract class WorkerBase { await this.events.start(); this.offsetStore = new OffsetStore(this.events, this.cfg, this.logger); - await Promise.all(Object.entries(kafkaCfg.topics).map(async ([key, value]: any[]) => { + for (const [key, value] of Object.entries(kafkaCfg.topics ?? {})) { const topicName = value.topic; const topic = await this.events.topic(topicName); const offsetValue = await this.offsetStore.getOffset(topicName); this.logger?.verbose('subscribing to topic with offset value', topicName, offsetValue); - Object.entries(value.events as { [key: string]: string } ?? {}).forEach( - ([eventName, handler]) => { - const i = handler.lastIndexOf('.'); - const name = handler.slice(0, i); - const serviceName = serviceNames?.[name] ?? name; - const functionName = handler.slice(i + 1); - this.eventHandlers.set(eventName, this.bindHandler(serviceName, functionName)); - topic.on( - eventName as string, - this.eventHandlers.get(eventName), - { startingOffset: offsetValue } - ); - } - ); + for (const [eventName, handler] of Object.entries(value.events ?? {})) { + const i = handler.lastIndexOf('.'); + const name = handler.slice(0, i); + const serviceName = serviceNames?.[name] ?? name; + const functionName = handler.slice(i + 1); + this.eventHandlers.set(eventName, this.bindHandler(serviceName, functionName)); + await topic.on( + eventName as string, + this.eventHandlers.get(eventName), + { startingOffset: BigInt(offsetValue) } + ); + } this.topics.set(key, topic); - })); + } } protected async bindScheduledJobs() { const job_config = this.cfg.get('scs-jobs'); From 4de20192fbd011341e41cfcb25a5c819eb2cd4b5 Mon Sep 17 00:00:00 2001 From: Gerald Baulig Date: Thu, 29 Jan 2026 16:04:55 +0100 Subject: [PATCH 3/3] fix(facade): hotfix for oidc login. TODO: Find alternative for oidc-provider --- package-lock.json | 207 +++++++----------- packages/facade/.gitignore | 4 +- packages/facade/cfg/config.json | 12 +- packages/facade/cfg/config_test.json | 9 - packages/facade/{debug-run.ts => debug.ts} | 0 packages/facade/esbuild.mjs | 13 ++ packages/facade/package.json | 15 +- packages/facade/src/modules/identity/index.ts | 1 - .../src/modules/identity/oidc/adapter.ts | 2 +- .../facade/src/modules/identity/oidc/index.ts | 20 +- .../src/modules/identity/oidc/interfaces.ts | 5 +- .../modules/identity/oidc/password-grant.ts | 82 +++---- .../src/modules/identity/oidc/router.ts | 8 +- .../src/modules/identity/oidc/templates.ts | 11 +- packages/facade/tests/facade.spec.ts | 1 - packages/facade/tests/facade.ts | 7 +- packages/facade/tests/oidc.spec.ts | 50 +++++ packages/facade/tests/token-service-stub.ts | 109 --------- packages/facade/tsconfig.debug.json | 7 +- packages/facade/tsconfig.test.json | 3 +- 20 files changed, 236 insertions(+), 330 deletions(-) delete mode 100644 packages/facade/cfg/config_test.json rename packages/facade/{debug-run.ts => debug.ts} (100%) create mode 100644 packages/facade/esbuild.mjs create mode 100644 packages/facade/tests/oidc.spec.ts delete mode 100644 packages/facade/tests/token-service-stub.ts diff --git a/package-lock.json b/package-lock.json index d1346934..e71fafb8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3561,27 +3561,6 @@ "node": ">= 14.0.0" } }, - "node_modules/@koa/router": { - "version": "13.1.1", - "resolved": "https://registry.npmjs.org/@koa/router/-/router-13.1.1.tgz", - "integrity": "sha512-JQEuMANYRVHs7lm7KY9PCIjkgJk73h4m4J+g2mkw2Vo1ugPZ17UJVqEH8F+HeAdjKz5do1OaLe7ArDz+z308gw==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.1", - "http-errors": "^2.0.0", - "koa-compose": "^4.1.0", - "path-to-regexp": "^6.3.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/@koa/router/node_modules/path-to-regexp": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", - "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", - "license": "MIT" - }, "node_modules/@lerna/create": { "version": "8.2.4", "resolved": "https://registry.npmjs.org/@lerna/create/-/create-8.2.4.tgz", @@ -7506,9 +7485,9 @@ "license": "MIT" }, "node_modules/@types/http-cache-semantics": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", - "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==", "license": "MIT" }, "node_modules/@types/http-errors": { @@ -13343,18 +13322,6 @@ "node": ">=0.10.0" } }, - "node_modules/eta": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/eta/-/eta-3.5.0.tgz", - "integrity": "sha512-e3x3FBvGzeCIHhF+zhK8FZA2vC5uFn6b4HJjegUbIWrDb4mJ7JjTGMJY9VGIbRVpmSwHopNiaJibhjIr+HfLug==", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "url": "https://github.com/eta-dev/eta?sponsor=1" - } - }, "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -20676,9 +20643,9 @@ } }, "node_modules/normalize-url": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.1.0.tgz", - "integrity": "sha512-X06Mfd/5aKsRHc0O0J5CUedwnPmnDtLF2+nq+KN9KSDlJHkPuh0JUviWjEWMe0SW/9TDdSLVPuk7L5gGTIA1/w==", + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.1.1.tgz", + "integrity": "sha512-JYc0DPlpGWB40kH5g07gGTrYuMqV653k3uBKY6uITPWds3M0ov3GaWGp9lbE3Bzngx8+XkfzgvASb9vk9JDFXQ==", "license": "MIT", "engines": { "node": ">=14.16" @@ -21365,77 +21332,10 @@ "integrity": "sha512-42CPE9AhahZRsMNslczq0ctAEtqk8Eka26QofnqC346BZdHDySk3LWka23LI7ULIw11NmltpiLagIq8gBozxTw==", "license": "MIT" }, - "node_modules/oidc-provider": { - "version": "8.8.1", - "resolved": "https://registry.npmjs.org/oidc-provider/-/oidc-provider-8.8.1.tgz", - "integrity": "sha512-qVChpayTwojUREJxLkFofUSK8kiSRIdzPrVSsoGibqRHl/YO60ege94OZS8vh7zaK+zxcG/Gu8UMaYB5ulohCQ==", - "license": "MIT", - "dependencies": { - "@koa/cors": "^5.0.0", - "@koa/router": "^13.1.0", - "debug": "^4.4.0", - "eta": "^3.5.0", - "got": "^13.0.0", - "jose": "^5.9.6", - "jsesc": "^3.1.0", - "koa": "^2.15.4", - "nanoid": "^5.0.9", - "object-hash": "^3.0.0", - "oidc-token-hash": "^5.0.3", - "quick-lru": "^7.0.0", - "raw-body": "^3.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/panva" - } - }, - "node_modules/oidc-provider/node_modules/iconv-lite": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", - "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/oidc-provider/node_modules/quick-lru": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-7.3.0.tgz", - "integrity": "sha512-k9lSsjl36EJdK7I06v7APZCbyGT2vMTsYSRX1Q2nbYmnkBqgUhRkAuzH08Ciotteu/PLJmIF2+tti7o3C/ts2g==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/oidc-provider/node_modules/raw-body": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.1.tgz", - "integrity": "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==", - "license": "MIT", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.7.0", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.10" - } - }, "node_modules/oidc-token-hash": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.1.1.tgz", - "integrity": "sha512-D7EmwxJV6DsEB6vOFLrBM2OzsVgQzgPWyHlV2OOAVj772n+WTXpudC9e9u5BVKQnYwaD30Ivhi9b+4UeBcGu9g==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.2.0.tgz", + "integrity": "sha512-6gj2m8cJZ+iSW8bm0FXdGF0YhIQbKrfP4yWTNzxc31U6MOjfEmB1rHvlYvxI1B7t7BCi1F2vYTT6YhtQRG4hxw==", "license": "MIT", "engines": { "node": "^10.13.0 || >=12.0.0" @@ -26450,19 +26350,6 @@ "node": ">= 0.4.0" } }, - "node_modules/uuid": { - "version": "11.0.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.2.tgz", - "integrity": "sha512-14FfcOJmqdjbBPdDjFQyk/SdT4NySW4eM0zcG+HqbHP5jzuH56xO3J1DGhgs/cEMCfwYi3HQI1gnTO62iaG+tQ==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/esm/bin/uuid" - } - }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", @@ -27746,13 +27633,11 @@ "nice-grpc": "^2.1.7", "node-fetch": "^3.3.1", "oauth": "^0.10.0", - "oidc-provider": "^8.4.6", + "oidc-provider": "8.4.6", "request-ip": "^3.3.0", "rxjs": "^7.8.1", "ts-proto-descriptors": "2.0.0", "ua-parser-js": "^2.0.0", - "uuid": "11.0.2", - "winston": "^3.9.0", "ws": "^8.18.3" }, "devDependencies": { @@ -27781,7 +27666,6 @@ "@types/request-ip": "^0.0.41", "@types/supertest": "^6.0.2", "@types/useragent": "^2.3.1", - "@types/uuid": "10.0.0", "@types/ws": "^8.18.1", "eslint": "^9.36.0", "nodemon": "^3.1.3", @@ -27794,6 +27678,23 @@ "vitest": "^3.2.4" } }, + "packages/facade/node_modules/@koa/router": { + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/@koa/router/-/router-12.0.2.tgz", + "integrity": "sha512-sYcHglGKTxGF+hQ6x67xDfkE9o+NhVlRHBqq6gLywaMc6CojK/5vFZByphdonKinYlMLkEkacm+HEse9HzwgTA==", + "deprecated": "Please upgrade to v15 or higher. All reported bugs in this version are fixed in newer releases, dependencies have been updated, and security has been improved.", + "license": "MIT", + "dependencies": { + "debug": "^4.3.4", + "http-errors": "^2.0.0", + "koa-compose": "^4.1.0", + "methods": "^1.1.2", + "path-to-regexp": "^6.3.0" + }, + "engines": { + "node": ">= 12" + } + }, "packages/facade/node_modules/@types/node": { "version": "22.8.6", "dev": true, @@ -27802,6 +27703,18 @@ "undici-types": "~6.19.8" } }, + "packages/facade/node_modules/eta": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/eta/-/eta-3.5.0.tgz", + "integrity": "sha512-e3x3FBvGzeCIHhF+zhK8FZA2vC5uFn6b4HJjegUbIWrDb4mJ7JjTGMJY9VGIbRVpmSwHopNiaJibhjIr+HfLug==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + }, + "funding": { + "url": "https://github.com/eta-dev/eta?sponsor=1" + } + }, "packages/facade/node_modules/glob": { "version": "11.0.3", "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz", @@ -27849,6 +27762,48 @@ "url": "https://github.com/sponsors/isaacs" } }, + "packages/facade/node_modules/oidc-provider": { + "version": "8.4.6", + "resolved": "https://registry.npmjs.org/oidc-provider/-/oidc-provider-8.4.6.tgz", + "integrity": "sha512-liuHBXRaIjer6nPGWagrl5UjPhIZqahqLVPoYlc2WXsRR7XddwNCBUl1ks5r3Q3uCUfMdQTv1VsjmlaObdff8w==", + "license": "MIT", + "dependencies": { + "@koa/cors": "^5.0.0", + "@koa/router": "^12.0.1", + "debug": "^4.3.4", + "eta": "^3.4.0", + "got": "^13.0.0", + "jose": "^5.2.4", + "jsesc": "^3.0.2", + "koa": "^2.15.3", + "nanoid": "^5.0.7", + "object-hash": "^3.0.0", + "oidc-token-hash": "^5.0.3", + "quick-lru": "^7.0.0", + "raw-body": "^2.5.2" + }, + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "packages/facade/node_modules/path-to-regexp": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "license": "MIT" + }, + "packages/facade/node_modules/quick-lru": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-7.3.0.tgz", + "integrity": "sha512-k9lSsjl36EJdK7I06v7APZCbyGT2vMTsYSRX1Q2nbYmnkBqgUhRkAuzH08Ciotteu/PLJmIF2+tti7o3C/ts2g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "packages/facade/node_modules/rimraf": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz", diff --git a/packages/facade/.gitignore b/packages/facade/.gitignore index 1fb421de..adce1c78 100644 --- a/packages/facade/.gitignore +++ b/packages/facade/.gitignore @@ -1,4 +1,6 @@ +build dist coverage codegen -*.generated.ts \ No newline at end of file +*.generated.ts +*.cjs \ No newline at end of file diff --git a/packages/facade/cfg/config.json b/packages/facade/cfg/config.json index e49d5634..6cf1e4f2 100644 --- a/packages/facade/cfg/config.json +++ b/packages/facade/cfg/config.json @@ -8,16 +8,8 @@ } }, "facade": { - "keys": [ - "secret" - ], "hostname": "0.0.0.0", - "port": 5000, - "kafka": { - "brokers": [ - "localhost:29092" - ] - } + "port": 5000 }, "resources": { "client": { @@ -92,7 +84,7 @@ }, "endpoint": "os" }, - "odic": { + "oidc": { "templates": { "layout": "./node_modules/@restorecommerce/facade/templates/layout.hbs", "login": "./node_modules/@restorecommerce/facade/templates/login.hbs", diff --git a/packages/facade/cfg/config_test.json b/packages/facade/cfg/config_test.json deleted file mode 100644 index a1b36a26..00000000 --- a/packages/facade/cfg/config_test.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "odic": { - "templates": { - "layout": "./templates/layout.hbs", - "login": "./templates/login.hbs", - "consent": "./templates/consent.hbs" - } - } -} \ No newline at end of file diff --git a/packages/facade/debug-run.ts b/packages/facade/debug.ts similarity index 100% rename from packages/facade/debug-run.ts rename to packages/facade/debug.ts diff --git a/packages/facade/esbuild.mjs b/packages/facade/esbuild.mjs new file mode 100644 index 00000000..6b05da03 --- /dev/null +++ b/packages/facade/esbuild.mjs @@ -0,0 +1,13 @@ +import * as esbuild from 'esbuild' +import { commonifierPlugin } from '@restorecommerce/dev' + +await esbuild.build({ + entryPoints: ['./tests/server.ts'], + bundle: true, + platform: 'node', + outfile: './tests/build/server.cjs', + minify: true, + treeShaking: true, + sourcemap: 'linked', + plugins: [commonifierPlugin], +}); \ No newline at end of file diff --git a/packages/facade/package.json b/packages/facade/package.json index c506831a..921a2420 100644 --- a/packages/facade/package.json +++ b/packages/facade/package.json @@ -55,13 +55,11 @@ "nice-grpc": "^2.1.7", "node-fetch": "^3.3.1", "oauth": "^0.10.0", - "oidc-provider": "^8.4.6", + "oidc-provider": "8.4.6", "request-ip": "^3.3.0", "rxjs": "^7.8.1", "ts-proto-descriptors": "2.0.0", "ua-parser-js": "^2.0.0", - "uuid": "11.0.2", - "winston": "^3.9.0", "ws": "^8.18.3" }, "devDependencies": { @@ -90,7 +88,6 @@ "@types/request-ip": "^0.0.41", "@types/supertest": "^6.0.2", "@types/useragent": "^2.3.1", - "@types/uuid": "10.0.0", "@types/ws": "^8.18.1", "eslint": "^9.36.0", "nodemon": "^3.1.3", @@ -104,19 +101,21 @@ }, "scripts": { "build": "npm-run-all build:clean generate build:compile build:codegen:clean build:codegen:compile", - "build:clean": "rimraf -rf ./dist", + "build:clean": "rimraf -rf ./dist ./tests/build/", "build:compile": "tsc -p tsconfig.lib.json", "build:codegen:clean": "rimraf -rf ./codegen", "build:codegen:compile": "tsc -p tsconfig.codegen.json", + "build:es": "npm-run-all build:codegen:clean build:codegen:compile && node esbuild.mjs", "dev": "npm-run-all build:clean dev:compile", - "dev:compile": "tsc -w -p tsconfig.json", + "dev:compile": "tsc -p tsconfig.json", "postinstall": "rm -rf ../../node_modules/@josephg/resolvable/index.ts", "test": "NODE_OPTIONS=--experimental-vm-modules vitest run", "test:watch": "vitest watch", - "dev:serve": "tsx watch --tsconfig tsconfig.test.json tests/server.ts", + "dev:serve": "NODE_OPTIONS=--experimental-vm-modules tsx watch --tsconfig tsconfig.test.json tests/server.ts", + "dev:serve:es": "npm-run-all build:es && node ./tests/build/server.cjs", "generate": "tsx --tsconfig tsconfig.generate.json generate.ts", "prepublishOnly": "npm run build", - "debug-run": "tsx --tsconfig tsconfig.debug.json debug-run.ts", + "debug": "tsx --tsconfig tsconfig.debug.json debug.ts", "lint": "eslint './**/*.ts'" }, "publishConfig": { diff --git a/packages/facade/src/modules/identity/index.ts b/packages/facade/src/modules/identity/index.ts index 7ddde6c9..fb5ce800 100644 --- a/packages/facade/src/modules/identity/index.ts +++ b/packages/facade/src/modules/identity/index.ts @@ -22,7 +22,6 @@ export const identityModule = createFacadeModuleFactory { try { const user = await findUserById(userService, id); + logger.debug('User found:', user); return { sub: id, data: user }; - } catch (error) { - logger.error('OIDC findAccount claims error', error); + } catch (error: any) { + const { code, message, stack } = error; + logger.error('OIDC findAccount claims error', { code, message, stack }); return { sub: id, data: { @@ -91,8 +93,9 @@ export function createOIDC({ } }, }; - } catch (error) { - logger.error('OIDC findAccount error', error); + } catch (error: any) { + const { code, message, stack } = error; + logger.error('OIDC findAccount error', { code, message, stack }); } }, claims: { @@ -126,8 +129,7 @@ export function createOIDC({ enabled: true }, devInteractions: { - // enabled: dev ?? false - enabled: false + enabled: false // env === 'development' }, }, clientBasedCORS: () => true @@ -148,7 +150,7 @@ export function createOIDC({ authLogService: identitySrvClient.authentication_log, authenticate: loginUserCredentials, provider - }); + }, logger); // Disable forbidding redirect to http/localhost in dev mode if (env === 'development') { @@ -166,6 +168,6 @@ export function createOIDC({ return { provider, - router + router, }; } diff --git a/packages/facade/src/modules/identity/oidc/interfaces.ts b/packages/facade/src/modules/identity/oidc/interfaces.ts index 8d89b412..9268f5e4 100644 --- a/packages/facade/src/modules/identity/oidc/interfaces.ts +++ b/packages/facade/src/modules/identity/oidc/interfaces.ts @@ -1,4 +1,4 @@ -import { type Adapter, errors } from 'oidc-provider'; +import { AccountClaims, type Adapter, errors } from 'oidc-provider'; import type Provider from 'oidc-provider'; import { type IdentityContext } from '../interfaces.js'; import { type AuthenticationLogServiceClient as authLogService } from '@restorecommerce/rc-grpc-clients/dist/generated/io/restorecommerce/authentication_log.js'; @@ -95,7 +95,8 @@ export class InvalidPasswordGrant extends errors.InvalidGrant { } } -export interface Claims { + +export interface Claims extends AccountClaims { sub: string | undefined; data: AuthUser; [key: string]: any; diff --git a/packages/facade/src/modules/identity/oidc/password-grant.ts b/packages/facade/src/modules/identity/oidc/password-grant.ts index a4a00799..b62922f7 100644 --- a/packages/facade/src/modules/identity/oidc/password-grant.ts +++ b/packages/facade/src/modules/identity/oidc/password-grant.ts @@ -1,23 +1,24 @@ -import type Koa from 'koa'; import { type TokenResponseBody, InvalidPasswordGrant, type OIDCPasswordGrantTypeConfig, - type Claims, - type LoginFnResponse + type LoginFnResponse, + Claims } from './interfaces.js'; import { nanoid, epochTime } from './utils.js'; import { UAParser } from 'ua-parser-js'; -import * as uuid from 'uuid'; import * as requestIp from 'request-ip'; import { AuthenticationLog, AuthenticationLogList } from '@restorecommerce/rc-grpc-clients/dist/generated/io/restorecommerce/authentication_log.js'; import { Subject } from '@restorecommerce/rc-grpc-clients/dist/generated/io/restorecommerce/auth.js'; +import { Logger } from '@restorecommerce/logger'; +import { ClaimsParameter, KoaContextWithOIDC } from 'oidc-provider'; +import { randomUUID } from 'crypto'; -export const registerPasswordGrantType = (config: OIDCPasswordGrantTypeConfig) => { - const performPasswordGrant = async (ctx: Koa.Context, clientId: string, identifier: string, password: string, key: string): Promise => { +export const registerPasswordGrantType = (config: OIDCPasswordGrantTypeConfig, logger?: Logger) => { + const performPasswordGrant = async (ctx: KoaContextWithOIDC, clientId: string, identifier: string, password: string, key: string): Promise => { const client = await ctx.oidc.provider.Client.find(clientId); let account: LoginFnResponse; @@ -42,12 +43,7 @@ export const registerPasswordGrantType = (config: OIDCPasswordGrantTypeConfig) = throw new InvalidPasswordGrant('invalid credentials provided'); } - let expiresIn = config.tokenExpiration; - if (!expiresIn) { - // default value of 1 day expiration when not set in config - expiresIn = 86400; - } - + const expiresIn = config.tokenExpiration || 86400; const claims: Claims = { sub: account.user.id, data: account.user @@ -55,51 +51,55 @@ export const registerPasswordGrantType = (config: OIDCPasswordGrantTypeConfig) = const {AccessToken} = ctx.oidc.provider; // for interactive login (to update user data in arangodb with token name) - let tokenName = uuid.v4().replace(/-/g, ''); + const tokenName = randomUUID().replace(/-/g, ''); claims.token_name = tokenName; - let defaultScope = claims.data.defaultScope; + const defaultScope = account.user.defaultScope; const at = new AccessToken({ gty: 'password', scope: 'openid', accountId: account.user.id, - claims, + claims: (claims as any), client, - grantId: ctx.oidc.uid, + grantId: (ctx.oidc as any).uid, expiresWithSession: false, - expiresIn + expiresIn, }); ctx.oidc.entity('AccessToken', at); + Object.assign(at, { + constructor: { + name: 'AccessToken', + IN_PAYLOAD: AccessToken.IN_PAYLOAD, + } + }); const accessToken = await at.save(); + const last_access = account.user?.lastAccess ? new Date(account.user.lastAccess) : undefined; - let last_access; - if (claims?.data?.lastAccess) { - last_access = new Date(claims.data.lastAccess); - } - - if (claims?.data?.tokens) { + if (account.user?.tokens) { claims.data = { ...claims.data, tokens: [] }; } - const generateIdToken = async (ctx: Koa.Context, clientId: string, expiresIn: number, claims: Claims): Promise => { + const generateIdToken = async (ctx: KoaContextWithOIDC, clientId: string, expiresIn: number, claims: Claims): Promise => { const client = await ctx.oidc.provider.Client.find(clientId); ctx.oidc.entity('Client', client); const {IdToken} = ctx.oidc.provider; const jti = nanoid(); const exp = epochTime() + expiresIn; - const token = new IdToken({ - ...claims, - }, {ctx}); + const token = new IdToken( + {...claims}, + {ctx} + ); token.set('jti', jti); - token.scope = 'openid profile'; + token.set('scope', 'openid profile'); return await token.issue({use: 'idtoken', expiresAt: exp}); }; const idToken = await generateIdToken(ctx, clientId, expiresIn, claims); + logger?.debug('ID Token granted:', idToken); return { access_token: accessToken, id_token: idToken, @@ -115,7 +115,7 @@ export const registerPasswordGrantType = (config: OIDCPasswordGrantTypeConfig) = config.provider.registerGrantType( 'password', - async (ctx: any, next: () => Promise) => { + async (ctx: KoaContextWithOIDC, next: () => Promise) => { try { const {body, client} = ctx.oidc; ctx.type = 'json'; @@ -128,19 +128,19 @@ export const registerPasswordGrantType = (config: OIDCPasswordGrantTypeConfig) = key = 'token'; } const req = ctx.request; - let os, agentName; + let os: string, agentName: string; const agent = new UAParser(req.headers['user-agent']); if (agent) { os = agent.getOS().toString(); agentName = agent.getUA(); } - ctx.body = await performPasswordGrant(ctx, client.clientId, body.identifier, passwordValue, key); - - const token_name = (ctx.body as TokenResponseBody).token_name; - const token = (ctx.body as TokenResponseBody).access_token; - const scope = (ctx.body as TokenResponseBody).default_scope; - let ipv4_address, ipv6_address; + const resp_body = await performPasswordGrant(ctx, client.clientId, (body as any).identifier, passwordValue, key); + ctx.body = resp_body; + const token_name = resp_body.token_name; + const token = resp_body.access_token; + const scope = resp_body.default_scope; + let ipv4_address: string, ipv6_address: string; const clientIP = requestIp.getClientIp(req.req); if (clientIP && clientIP.includes('.')) { ipv4_address = clientIP; @@ -158,19 +158,21 @@ export const registerPasswordGrantType = (config: OIDCPasswordGrantTypeConfig) = tokenName: token_name }); - await config.authLogService.create(AuthenticationLogList.fromPartial({ + config.authLogService.create(AuthenticationLogList.fromPartial({ items: [authLogItem], subject: Subject.fromPartial({token, scope}) as Subject })); - } catch (ex) { + } catch (ex: any) { if (ex instanceof InvalidPasswordGrant) { ctx.status = 401; ctx.type = 'json'; ctx.body = { - error: ex['error'], - error_description: ex['error_description'] + error: ex.error, + error_description: ex.error_description, }; } else { + const { code, error, message, error_description, stack } = ex; + logger?.error('OIDC:', { code, error, message, error_description, stack }); ctx.status = 400; ctx.body = { error: 'bad_request', diff --git a/packages/facade/src/modules/identity/oidc/router.ts b/packages/facade/src/modules/identity/oidc/router.ts index 368f0ef3..67cf3962 100644 --- a/packages/facade/src/modules/identity/oidc/router.ts +++ b/packages/facade/src/modules/identity/oidc/router.ts @@ -1,7 +1,7 @@ import type KoaRouter from 'koa-router'; import { type InteractionResults } from 'oidc-provider'; import type Provider from 'oidc-provider'; -import { type Logger } from 'winston'; +import { type Logger } from '@restorecommerce/logger'; import { type IdentityContext } from '../interfaces.js'; import { OIDCTemplateEngine } from './templates.js'; // import { AuthUser, loginUser } from './user/index.js'; @@ -21,7 +21,7 @@ export const createOIDCRouter = ({logger, loginFn, provider, env, templates }: C const dev = env === 'development'; - const tplEngine = new OIDCTemplateEngine(templates); + const tplEngine = new OIDCTemplateEngine(templates, logger); const router = new Router() as KoaRouter<{}, IdentityContext>; @@ -47,7 +47,7 @@ export const createOIDCRouter = ({logger, loginFn, provider, env, templates }: C return; } case 'consent': { - console.log('consent', prompt.details); + logger.debug('consent', prompt.details); const { prompt: { name, details } } = await provider.interactionDetails(ctx.req, ctx.res); @@ -135,7 +135,7 @@ export const createOIDCRouter = ({logger, loginFn, provider, env, templates }: C return render(); } - const { error, user, identifier, remember } = await loginFn(ctx, body); + const { error, user, identifier, remember } = await loginFn(ctx, body); if (error || !user) { logger.error('OIDC login callback error', error); diff --git a/packages/facade/src/modules/identity/oidc/templates.ts b/packages/facade/src/modules/identity/oidc/templates.ts index c6fb67b2..49c02556 100644 --- a/packages/facade/src/modules/identity/oidc/templates.ts +++ b/packages/facade/src/modules/identity/oidc/templates.ts @@ -2,6 +2,7 @@ import path from 'node:path'; import fs from 'node:fs'; import hbs from 'handlebars'; import { type OIDCHbsTemplates } from './interfaces.js'; +import { Logger } from '@restorecommerce/logger'; export interface OIDCTemplateError { key: string; @@ -39,11 +40,15 @@ export class OIDCTemplateEngine { private loginHbs?: HandlebarsTemplateDelegate; private consentHbs?: HandlebarsTemplateDelegate; - constructor(private templates: OIDCHbsTemplates | undefined) { } + constructor( + private templates?: OIDCHbsTemplates, + private logger?: Logger, + ) { } async load(target: string) { const template = this.templates?.[target]; if (template) { + this.logger?.info(`OIDC: Loading template ${template}`); const layout = await new Promise((resolve, reject) => { fs.readFile( path.resolve(template), @@ -53,7 +58,9 @@ export class OIDCTemplateEngine { return hbs.compile(layout); } else { - throw new Error(`OIDC 'odic.template.${target}' not configured!`); + const msg = `OIDC: 'odic.template.${target}' not configured!`; + this.logger?.warn(msg); + throw new Error(msg); } } diff --git a/packages/facade/tests/facade.spec.ts b/packages/facade/tests/facade.spec.ts index 26929fab..42a8b89a 100644 --- a/packages/facade/tests/facade.spec.ts +++ b/packages/facade/tests/facade.spec.ts @@ -6,7 +6,6 @@ describe('facade', () => { await facade.start(); }); - it('should start the facade', () => { expect(facade).toBeTruthy(); expect(facade.listening).toBe(true); diff --git a/packages/facade/tests/facade.ts b/packages/facade/tests/facade.ts index 652c3f3a..fc55054a 100644 --- a/packages/facade/tests/facade.ts +++ b/packages/facade/tests/facade.ts @@ -15,14 +15,14 @@ const createTestFacade = () => { facade: serviceConfig.get('facade'), resources: serviceConfig.get('resources'), identity: serviceConfig.get('identity'), - example: {message: 'foo'} + example: {message: 'foo'}, + oidc: serviceConfig.get('oidc') }; const logger = createLogger(cfg.logger); return createFacade({ - // ...cfg.facade, - + ...cfg.facade, env: cfg.env, logger }) @@ -44,6 +44,7 @@ const createTestFacade = () => { 'http://localhost:4200' ], jwks, + ...cfg.oidc, } })) .useMiddleware(reqResLogger({logger})); diff --git a/packages/facade/tests/oidc.spec.ts b/packages/facade/tests/oidc.spec.ts new file mode 100644 index 00000000..de0af5ae --- /dev/null +++ b/packages/facade/tests/oidc.spec.ts @@ -0,0 +1,50 @@ +import { facade } from './facade.js'; +import { agent } from 'supertest'; +import { it, describe, beforeAll, afterAll, expect } from 'vitest'; + +describe('extend', () => { + beforeAll(async () => { + await facade.start(); + }); + + it('should start the facade', () => { + expect(facade).toBeTruthy(); + expect(facade.listening).toBe(true); + }); + + it('should request a token', () => { + const request = agent(facade.server); + const params = new URLSearchParams({ + identifier: 'nfuse-root.admin', + password: 'CNQJrH%KAayeDpf3h', + grant_type: 'password', + scope: 'openid', + }).toString(); + return new Promise((resolve, reject) => { + request + .post('/token') + .send(params) + .set('Authorization', 'Basic VEVTVF9DTElFTlRfSUQ6VEVTVF9DTElFTlRfU0VDUkVU=') + .set('Accept', 'application/json') + .set('Content-Type', 'application/x-www-form-urlencoded') + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + if (err) { + reject(err); + } + try { + expect(res.body).toBeInstanceOf(Object); + expect(res.body.id_token).toBeDefined(); + } catch(err) { + reject(err); + } + resolve(); + }); + }); + }); + + afterAll(async () => { + await facade.stop(); + }); +}); \ No newline at end of file diff --git a/packages/facade/tests/token-service-stub.ts b/packages/facade/tests/token-service-stub.ts deleted file mode 100644 index 115d6072..00000000 --- a/packages/facade/tests/token-service-stub.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { type Any, type TokenService } from '@restorecommerce/rc-grpc-clients'; -import LRU from 'lru-cache'; -import { type AdapterPayload } from 'oidc-provider'; -import { epochTime, marshallProtobufAny, unmarshallProtobufAny } from './utils.js'; - -export interface FindRequest { - type: string; - id: string; -} - -export interface DestroyRequest { - type: string; - id: string; -} - -export interface ConsumeRequest { - type: string; - id: string; -} - -export interface UpsertRequest { - type: string; - id: string; - payload: Any; - expiresIn: number; -} - -export interface RevokeByGrantIdRequest { - grantId: string; -} - -// export interface TokenService { -// consume(args: ConsumeRequest): Promise; -// destroy(args: DestroyRequest): Promise; -// find(args: FindRequest): Promise; -// upsert(args: UpsertRequest): Promise; -// revokeByGrantId(args: RevokeByGrantIdRequest): Promise; -// } - -export class TokenServiceStub implements TokenServiceImplementation { - tokenStorage = new LRUCache({ max: 1000 }); - grantIdStorage = new LRUCache({ max: 1000 }); - - private key(type: string, id: string) { - return `${type}:${id}`; - } - async consume({ type, id }: ConsumeRequest): Promise { - console.log('[ids] consume', ...arguments); - const payload = this.tokenStorage.get(this.key(type, id)); - if (payload) { - payload.consumed = epochTime(); - } - return { - typeUrl: '', - value: Buffer.from('{}') - }; - } - async destroy({ type, id }: DestroyRequest): Promise { - console.log('[ids] destroy', ...arguments); - this.tokenStorage.delete(this.key(type, id)); - return { - typeUrl: '', - value: Buffer.from('{}') - }; - - } - async find({ type, id }: FindRequest): Promise { - console.log('[ids] find', ...arguments); - const payload = this.tokenStorage.get(this.key(type, id)); - if (payload) { - return marshallProtobufAny(payload); - } - return undefined; - } - async upsert({ type, id, expiresIn, payload: payloadBuffer }: UpsertRequest): Promise { - console.log('[ids] upsert', ...arguments); - const payload = unmarshallProtobufAny(payloadBuffer); - - const key = this.key(type, id); - - const { grantId } = payload; - if (grantId) { - const grant = this.grantIdStorage.get(grantId); - if (!grant) { - this.grantIdStorage.set(grantId, [key]); - } else { - grant.push(key); - } - } - this.tokenStorage.set(key, payload, { ttl: expiresIn * 1000 }); - return { - typeUrl: '', - value: Buffer.from('{}') - }; - } - async revokeByGrantId({ grantId }: RevokeByGrantIdRequest): Promise { - console.log('[ids] revokeByGrantId', ...arguments); - const grant = this.grantIdStorage.get(grantId); - if (grant) { - grant.forEach((token: string) => this.tokenStorage.delete(token)); - this.grantIdStorage.delete(grantId); - } - return { - typeUrl: '', - value: Buffer.from('{}') - }; - } - -} diff --git a/packages/facade/tsconfig.debug.json b/packages/facade/tsconfig.debug.json index df67bb39..87a66a8c 100644 --- a/packages/facade/tsconfig.debug.json +++ b/packages/facade/tsconfig.debug.json @@ -1,11 +1,14 @@ { "extends": "./tsconfig-base.json", "compilerOptions": { - "outDir": "./dist" + "outDir": "./dist", + "types": ["node"], + "module": "ESNext", + "moduleResolution": "Node10" }, "include": [ "./src/**/*.ts", "./src/**/*.d.ts", - "./debug-run.ts" + "./debug.ts" ] } diff --git a/packages/facade/tsconfig.test.json b/packages/facade/tsconfig.test.json index 7baa0806..fad8d215 100644 --- a/packages/facade/tsconfig.test.json +++ b/packages/facade/tsconfig.test.json @@ -6,7 +6,6 @@ "moduleResolution": "Node10" }, "include": [ - "./tests/**/*.ts", - "tests/generate-keys.js" + "./tests/**/*.ts" ] }