fix(deps): migrate SSO OIDC code to openid-client v6 (#492)

* chore(deps): bump the all-npm-backend group across 1 directory with 4 updates

Bumps the all-npm-backend group with 4 updates in the /backend directory: [@aws-sdk/client-ecr](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-ecr), [openid-client](https://github.com/panva/openid-client), [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) and [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest).


Updates `@aws-sdk/client-ecr` from 3.1026.0 to 3.1028.0
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/clients/client-ecr/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.1028.0/clients/client-ecr)

Updates `openid-client` from 5.7.1 to 6.8.2
- [Release notes](https://github.com/panva/openid-client/releases)
- [Changelog](https://github.com/panva/openid-client/blob/main/CHANGELOG.md)
- [Commits](https://github.com/panva/openid-client/compare/v5.7.1...v6.8.2)

Updates `@types/node` from 25.5.2 to 25.6.0
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `vitest` from 4.1.3 to 4.1.4
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.4/packages/vitest)

---
updated-dependencies:
- dependency-name: "@aws-sdk/client-ecr"
  dependency-version: 3.1028.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-npm-backend
- dependency-name: openid-client
  dependency-version: 6.8.2
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: all-npm-backend
- dependency-name: "@types/node"
  dependency-version: 25.6.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: all-npm-backend
- dependency-name: vitest
  dependency-version: 4.1.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: all-npm-backend
...

Signed-off-by: dependabot[bot] <support@github.com>

* fix(deps): migrate SSO OIDC code to openid-client v6

The v5 Issuer/Client/generators API was removed upstream. Rewrite the
OIDC auth flow in SSOService to use the v6 functional API:

* discovery()/Configuration replace Issuer.discover + new issuer.Client
* buildAuthorizationUrl replaces client.authorizationUrl
* authorizationCodeGrant replaces client.callback, and handles the
  state check internally so the manual CSRF guard is removed
* fetchUserInfo replaces client.userinfo and now receives claims.sub
  so v6 can reject userinfo/id_token subject mismatches

Also:
* Cache the discovered Configuration per provider in CacheService
  (TTL 5 min) so a single login flow does not pay the HTTPS discovery
  round trip twice. Invalidation wired into saveProviderConfig,
  deleteProviderConfig, and seedOidcFromEnv via a private helper.
* Fix the testOidcDiscovery hack that passed a "discovery-probe"
  placeholder when clientId was missing; validate clientId upfront
  instead and return a clear error.
* Log fetchUserInfo failures at warn level before falling back to
  id_token claims so a subject-mismatch rejection is not silently
  hidden.

Unblocks dependabot PR #470.

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
This commit is contained in:
Anso
2026-04-10 16:28:26 -04:00
committed by GitHub
parent af4b17cc37
commit 12fe79fc85
4 changed files with 175 additions and 163 deletions
+6
View File
@@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Fixed
* **deps:** migrate SSO OIDC integration from `openid-client` v5 to v6. The v5 `Issuer` / `Client` / `generators` API was removed upstream; the service now uses `Configuration`, `discovery`, `buildAuthorizationUrl`, `authorizationCodeGrant`, and `fetchUserInfo`. Discovery metadata is cached for 5 minutes via `CacheService` so a single login flow does not pay the HTTPS round trip twice, and the cache is invalidated on every SSO config write. `fetchUserInfo` failures now log the underlying error instead of silently falling back to id_token claims.
## [0.43.0](https://github.com/AnsoCode/Sencho/compare/v0.42.7...v0.43.0) (2026-04-10)
+72 -101
View File
@@ -35,7 +35,7 @@
"jsonwebtoken": "^9.0.3",
"ldapts": "^8.1.7",
"node-pty": "^1.1.0",
"openid-client": "^5.7.1",
"openid-client": "^6.8.2",
"semver": "^7.7.4",
"systeminformation": "^5.31.1",
"ws": "^8.19.0",
@@ -187,9 +187,9 @@
}
},
"node_modules/@aws-sdk/client-ecr": {
"version": "3.1026.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/client-ecr/-/client-ecr-3.1026.0.tgz",
"integrity": "sha512-w9goy0kMoOeAT2HcZEpczW0kCrv2EcnS5qyehd6rU+OlbdjfY42gOKdUOy02RvS2SUqycQnecKgU7Q09gHdKuA==",
"version": "3.1028.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/client-ecr/-/client-ecr-3.1028.0.tgz",
"integrity": "sha512-NvnqmQxFtBC6eQKW5leLIfE8Go7GgBlIiFb9sfCVEMXg4i9rYAlb/Sd4reiH0AdWY/L2p+fCqOsG7Dw/DVwCoQ==",
"license": "Apache-2.0",
"dependencies": {
"@aws-crypto/sha256-browser": "5.2.0",
@@ -2168,12 +2168,12 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "25.5.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.2.tgz",
"integrity": "sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg==",
"version": "25.6.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz",
"integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==",
"license": "MIT",
"dependencies": {
"undici-types": "~7.18.0"
"undici-types": "~7.19.0"
}
},
"node_modules/@types/qs": {
@@ -2512,16 +2512,16 @@
}
},
"node_modules/@vitest/expect": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.3.tgz",
"integrity": "sha512-CW8Q9KMtXDGHj0vCsqui0M5KqRsu0zm0GNDW7Gd3U7nZ2RFpPKSCpeCXoT+/+5zr1TNlsoQRDEz+LzZUyq6gnQ==",
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.4.tgz",
"integrity": "sha512-iPBpra+VDuXmBFI3FMKHSFXp3Gx5HfmSCE8X67Dn+bwephCnQCaB7qWK2ldHa+8ncN8hJU8VTMcxjPpyMkUjww==",
"dev": true,
"license": "MIT",
"dependencies": {
"@standard-schema/spec": "^1.1.0",
"@types/chai": "^5.2.2",
"@vitest/spy": "4.1.3",
"@vitest/utils": "4.1.3",
"@vitest/spy": "4.1.4",
"@vitest/utils": "4.1.4",
"chai": "^6.2.2",
"tinyrainbow": "^3.1.0"
},
@@ -2530,13 +2530,13 @@
}
},
"node_modules/@vitest/mocker": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.3.tgz",
"integrity": "sha512-XN3TrycitDQSzGRnec/YWgoofkYRhouyVQj4YNsJ5r/STCUFqMrP4+oxEv3e7ZbLi4og5kIHrZwekDJgw6hcjw==",
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.4.tgz",
"integrity": "sha512-R9HTZBhW6yCSGbGQnDnH3QHfJxokKN4KB+Yvk9Q1le7eQNYwiCyKxmLmurSpFy6BzJanSLuEUDrD+j97Q+ZLPg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/spy": "4.1.3",
"@vitest/spy": "4.1.4",
"estree-walker": "^3.0.3",
"magic-string": "^0.30.21"
},
@@ -2557,9 +2557,9 @@
}
},
"node_modules/@vitest/pretty-format": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.3.tgz",
"integrity": "sha512-hYqqwuMbpkkBodpRh4k4cQSOELxXky1NfMmQvOfKvV8zQHz8x8Dla+2wzElkMkBvSAJX5TRGHJAQvK0TcOafwg==",
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.4.tgz",
"integrity": "sha512-ddmDHU0gjEUyEVLxtZa7xamrpIefdEETu3nZjWtHeZX4QxqJ7tRxSteHVXJOcr8jhiLoGAhkK4WJ3WqBpjx42A==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2570,13 +2570,13 @@
}
},
"node_modules/@vitest/runner": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.3.tgz",
"integrity": "sha512-VwgOz5MmT0KhlUj40h02LWDpUBVpflZ/b7xZFA25F29AJzIrE+SMuwzFf0b7t4EXdwRNX61C3B6auIXQTR3ttA==",
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.4.tgz",
"integrity": "sha512-xTp7VZ5aXP5ZJrn15UtJUWlx6qXLnGtF6jNxHepdPHpMfz/aVPx+htHtgcAL2mDXJgKhpoo2e9/hVJsIeFbytQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/utils": "4.1.3",
"@vitest/utils": "4.1.4",
"pathe": "^2.0.3"
},
"funding": {
@@ -2584,14 +2584,14 @@
}
},
"node_modules/@vitest/snapshot": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.3.tgz",
"integrity": "sha512-9l+k/J9KG5wPJDX9BcFFzhhwNjwkRb8RsnYhaT1vPY7OufxmQFc9sZzScRCPTiETzl37mrIWVY9zxzmdVeJwDQ==",
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.4.tgz",
"integrity": "sha512-MCjCFgaS8aZz+m5nTcEcgk/xhWv0rEH4Yl53PPlMXOZ1/Ka2VcZU6CJ+MgYCZbcJvzGhQRjVrGQNZqkGPttIKw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/pretty-format": "4.1.3",
"@vitest/utils": "4.1.3",
"@vitest/pretty-format": "4.1.4",
"@vitest/utils": "4.1.4",
"magic-string": "^0.30.21",
"pathe": "^2.0.3"
},
@@ -2600,9 +2600,9 @@
}
},
"node_modules/@vitest/spy": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.3.tgz",
"integrity": "sha512-ujj5Uwxagg4XUIfAUyRQxAg631BP6e9joRiN99mr48Bg9fRs+5mdUElhOoZ6rP5mBr8Bs3lmrREnkrQWkrsTCw==",
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.4.tgz",
"integrity": "sha512-XxNdAsKW7C+FLydqFJLb5KhJtl3PGCMmYwFRfhvIgxJvLSXhhVI1zM8f1qD3Zg7RCjTSzDVyct6sghs9UEgBEQ==",
"dev": true,
"license": "MIT",
"funding": {
@@ -2610,13 +2610,13 @@
}
},
"node_modules/@vitest/utils": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.3.tgz",
"integrity": "sha512-Pc/Oexse/khOWsGB+w3q4yzA4te7W4gpZZAvk+fr8qXfTURZUMj5i7kuxsNK5mP/dEB6ao3jfr0rs17fHhbHdw==",
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.4.tgz",
"integrity": "sha512-13QMT+eysM5uVGa1rG4kegGYNp6cnQcsTc67ELFbhNLQO+vgsygtYJx2khvdt4gVQqSSpC/KT5FZZxUpP3Oatw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/pretty-format": "4.1.3",
"@vitest/pretty-format": "4.1.4",
"convert-source-map": "^2.0.0",
"tinyrainbow": "^3.1.0"
},
@@ -4588,9 +4588,9 @@
"license": "ISC"
},
"node_modules/jose": {
"version": "4.15.9",
"resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz",
"integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==",
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/jose/-/jose-6.2.2.tgz",
"integrity": "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/panva"
@@ -5044,18 +5044,6 @@
"loose-envify": "cli.js"
}
},
"node_modules/lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"license": "ISC",
"dependencies": {
"yallist": "^4.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/luxon": {
"version": "3.7.2",
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz",
@@ -5357,6 +5345,15 @@
"node": ">=0.10.0"
}
},
"node_modules/oauth4webapi": {
"version": "3.8.5",
"resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-3.8.5.tgz",
"integrity": "sha512-A8jmyUckVhRJj5lspguklcl90Ydqk61H3dcU0oLhH3Yv13KpAliKTt5hknpGGPZSSfOwGyraNEFmofDYH+1kSg==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/panva"
}
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@@ -5366,15 +5363,6 @@
"node": ">=0.10.0"
}
},
"node_modules/object-hash": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz",
"integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==",
"license": "MIT",
"engines": {
"node": ">= 6"
}
},
"node_modules/object-inspect": {
"version": "1.13.4",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
@@ -5398,15 +5386,6 @@
],
"license": "MIT"
},
"node_modules/oidc-token-hash": {
"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"
}
},
"node_modules/on-finished": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
@@ -5438,15 +5417,13 @@
}
},
"node_modules/openid-client": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.7.1.tgz",
"integrity": "sha512-jDBPgSVfTnkIh71Hg9pRvtJc6wTwqjRkN88+gCFtYWrlP4Yx2Dsrow8uPi3qLr/aeymPF3o2+dS+wOpglK04ew==",
"version": "6.8.2",
"resolved": "https://registry.npmjs.org/openid-client/-/openid-client-6.8.2.tgz",
"integrity": "sha512-uOvTCndr4udZsKihJ68H9bUICrriHdUVJ6Az+4Ns6cW55rwM5h0bjVIzDz2SxgOI84LKjFyjOFvERLzdTUROGA==",
"license": "MIT",
"dependencies": {
"jose": "^4.15.9",
"lru-cache": "^6.0.0",
"object-hash": "^2.2.0",
"oidc-token-hash": "^5.0.3"
"jose": "^6.1.3",
"oauth4webapi": "^3.8.4"
},
"funding": {
"url": "https://github.com/sponsors/panva"
@@ -6606,9 +6583,9 @@
"license": "MIT"
},
"node_modules/undici-types": {
"version": "7.18.2",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
"integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
"version": "7.19.2",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz",
"integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==",
"license": "MIT"
},
"node_modules/unpipe": {
@@ -6757,19 +6734,19 @@
}
},
"node_modules/vitest": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.3.tgz",
"integrity": "sha512-DBc4Tx0MPNsqb9isoyOq00lHftVx/KIU44QOm2q59npZyLUkENn8TMFsuzuO+4U2FUa9rgbbPt3udrP25GcjXw==",
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.4.tgz",
"integrity": "sha512-tFuJqTxKb8AvfyqMfnavXdzfy3h3sWZRWwfluGbkeR7n0HUev+FmNgZ8SDrRBTVrVCjgH5cA21qGbCffMNtWvg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/expect": "4.1.3",
"@vitest/mocker": "4.1.3",
"@vitest/pretty-format": "4.1.3",
"@vitest/runner": "4.1.3",
"@vitest/snapshot": "4.1.3",
"@vitest/spy": "4.1.3",
"@vitest/utils": "4.1.3",
"@vitest/expect": "4.1.4",
"@vitest/mocker": "4.1.4",
"@vitest/pretty-format": "4.1.4",
"@vitest/runner": "4.1.4",
"@vitest/snapshot": "4.1.4",
"@vitest/spy": "4.1.4",
"@vitest/utils": "4.1.4",
"es-module-lexer": "^2.0.0",
"expect-type": "^1.3.0",
"magic-string": "^0.30.21",
@@ -6797,12 +6774,12 @@
"@edge-runtime/vm": "*",
"@opentelemetry/api": "^1.9.0",
"@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
"@vitest/browser-playwright": "4.1.3",
"@vitest/browser-preview": "4.1.3",
"@vitest/browser-webdriverio": "4.1.3",
"@vitest/coverage-istanbul": "4.1.3",
"@vitest/coverage-v8": "4.1.3",
"@vitest/ui": "4.1.3",
"@vitest/browser-playwright": "4.1.4",
"@vitest/browser-preview": "4.1.4",
"@vitest/browser-webdriverio": "4.1.4",
"@vitest/coverage-istanbul": "4.1.4",
"@vitest/coverage-v8": "4.1.4",
"@vitest/ui": "4.1.4",
"happy-dom": "*",
"jsdom": "*",
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
@@ -6955,12 +6932,6 @@
"node": ">=10"
}
},
"node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"license": "ISC"
},
"node_modules/yaml": {
"version": "2.8.3",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz",
+1 -1
View File
@@ -61,7 +61,7 @@
"jsonwebtoken": "^9.0.3",
"ldapts": "^8.1.7",
"node-pty": "^1.1.0",
"openid-client": "^5.7.1",
"openid-client": "^6.8.2",
"semver": "^7.7.4",
"systeminformation": "^5.31.1",
"ws": "^8.19.0",
+96 -61
View File
@@ -1,9 +1,25 @@
import crypto from 'crypto';
import { Client as LdapClient } from 'ldapts';
import { Issuer, Client as OIDCClient, generators } from 'openid-client';
import {
Configuration,
discovery,
buildAuthorizationUrl,
authorizationCodeGrant,
fetchUserInfo,
randomState,
randomPKCECodeVerifier,
calculatePKCECodeChallenge,
} from 'openid-client';
import { DatabaseService, User, AuthProvider } from './DatabaseService';
import { CryptoService } from './CryptoService';
import { LicenseService } from './LicenseService';
import { CacheService } from './CacheService';
// OIDC discovery metadata changes rarely; caching it eliminates the redundant
// HTTPS round-trip between getOIDCAuthorizationUrl and handleOIDCCallback in
// the same login flow, and across back-to-back logins.
const OIDC_CONFIG_TTL_MS = 5 * 60 * 1000;
const OIDC_CONFIG_NS = 'oidc-config';
export interface SSOProviderConfig {
provider: string;
@@ -121,6 +137,11 @@ export class SSOService {
configForStorage.oidcClientSecret = cryptoSvc.encrypt(configForStorage.oidcClientSecret);
}
db.upsertSSOConfig(provider, true, JSON.stringify(configForStorage));
this.invalidateOIDCConfigCache(provider);
}
private invalidateOIDCConfigCache(provider: string): void {
CacheService.getInstance().invalidate(`${OIDC_CONFIG_NS}:${provider}`);
}
// --- Config Management ---
@@ -173,10 +194,13 @@ export class SSOService {
config.enabled,
JSON.stringify(configForStorage)
);
// Client ID, secret, or issuer URL may have changed, so drop the cached Configuration.
this.invalidateOIDCConfigCache(config.provider);
}
public deleteProviderConfig(provider: string): void {
DatabaseService.getInstance().deleteSSOConfig(provider);
this.invalidateOIDCConfigCache(provider);
}
// --- LDAP Authentication ---
@@ -313,21 +337,22 @@ export class SSOService {
throw new Error(`SSO provider ${provider} is missing client ID`);
}
const { client } = await this.getOIDCClient(provider, config, callbackUrl);
const state = generators.state();
const codeVerifier = generators.codeVerifier();
const codeChallenge = generators.codeChallenge(codeVerifier);
const oidcConfig = await this.getOIDCConfig(provider, config);
const state = randomState();
const codeVerifier = randomPKCECodeVerifier();
const codeChallenge = await calculatePKCECodeChallenge(codeVerifier);
const scopes = config.oidcScopes || 'openid email profile';
const url = client.authorizationUrl({
const url = buildAuthorizationUrl(oidcConfig, {
redirect_uri: callbackUrl,
scope: scopes,
state,
code_challenge: codeChallenge,
code_challenge_method: 'S256',
});
return { url, state, codeVerifier };
return { url: url.href, state, codeVerifier };
}
public async handleOIDCCallback(
@@ -337,39 +362,43 @@ export class SSOService {
expectedState: string,
codeVerifier: string
): Promise<SSOAuthResult> {
if (params.state !== expectedState) {
return { success: false, error: 'Invalid state parameter (possible CSRF attack)' };
}
const config = this.getProviderConfigDecrypted(provider);
if (!config || !config.enabled) {
return { success: false, error: `SSO provider ${provider} is not configured` };
}
try {
const { client } = await this.getOIDCClient(provider, config, callbackUrl);
const oidcConfig = await this.getOIDCConfig(provider, config);
const tokenSet = await client.callback(callbackUrl, { code: params.code, state: params.state }, {
state: expectedState,
code_verifier: codeVerifier,
const currentUrl = new URL(callbackUrl);
currentUrl.searchParams.set('code', params.code);
currentUrl.searchParams.set('state', params.state);
const tokens = await authorizationCodeGrant(oidcConfig, currentUrl, {
pkceCodeVerifier: codeVerifier,
expectedState,
});
let userInfo: Record<string, unknown>;
if (provider === 'oidc_github') {
// GitHub doesn't support standard OIDC userinfo; use their API
userInfo = await this.fetchGitHubUserInfo(tokenSet.access_token as string);
} else if (tokenSet.id_token) {
const claims = tokenSet.claims();
// Also fetch userinfo for complete profile
try {
const info = await client.userinfo(tokenSet.access_token as string);
userInfo = { ...claims, ...info };
} catch {
userInfo = claims as Record<string, unknown>;
}
userInfo = await this.fetchGitHubUserInfo(tokens.access_token);
} else {
userInfo = await client.userinfo(tokenSet.access_token as string) as Record<string, unknown>;
const claims = tokens.claims();
if (!claims) {
return { success: false, error: 'OIDC provider did not return an ID token' };
}
// Pass claims.sub so v6 rejects userinfo/id_token subject mismatches.
try {
const info = await fetchUserInfo(oidcConfig, tokens.access_token, String(claims.sub));
userInfo = { ...claims, ...info };
} catch (err) {
// Log so a subject-mismatch rejection is not silently hidden.
const message = err instanceof Error ? err.message : String(err);
console.warn(`[SSO] fetchUserInfo failed, falling back to id_token claims: ${message}`);
userInfo = { ...claims };
}
}
const sub = String(userInfo.sub || userInfo.id || '');
@@ -424,38 +453,37 @@ export class SSOService {
};
}
private async getOIDCClient(
private async getOIDCConfig(
provider: string,
config: SSOProviderConfig,
callbackUrl: string
): Promise<{ client: OIDCClient; issuer: InstanceType<typeof Issuer> }> {
let issuer: InstanceType<typeof Issuer>;
): Promise<Configuration> {
const clientId = config.oidcClientId || '';
const clientSecret = config.oidcClientSecret || undefined;
if (provider === 'oidc_github') {
// GitHub is not a standard OIDC provider - manually configure
issuer = new Issuer({
issuer: 'https://github.com',
authorization_endpoint: 'https://github.com/login/oauth/authorize',
token_endpoint: 'https://github.com/login/oauth/access_token',
userinfo_endpoint: 'https://api.github.com/user',
});
} else {
const issuerUrl = config.oidcIssuerUrl || WELL_KNOWN_ISSUERS[provider];
if (!issuerUrl) {
throw new Error(`Issuer URL not configured for ${provider}`);
}
issuer = await Issuer.discover(issuerUrl);
// GitHub is not a standard OIDC provider. Construct Configuration
// directly from known endpoints instead of going through discovery.
return new Configuration(
{
issuer: 'https://github.com',
authorization_endpoint: 'https://github.com/login/oauth/authorize',
token_endpoint: 'https://github.com/login/oauth/access_token',
userinfo_endpoint: 'https://api.github.com/user',
},
clientId,
clientSecret,
);
}
const client = new issuer.Client({
client_id: config.oidcClientId || '',
client_secret: config.oidcClientSecret || '',
redirect_uris: [callbackUrl],
response_types: ['code'],
token_endpoint_auth_method: 'client_secret_post',
});
return { client, issuer };
const issuerUrl = config.oidcIssuerUrl || WELL_KNOWN_ISSUERS[provider];
if (!issuerUrl) {
throw new Error(`Issuer URL not configured for ${provider}`);
}
return CacheService.getInstance().getOrFetch(
`${OIDC_CONFIG_NS}:${provider}`,
OIDC_CONFIG_TTL_MS,
() => discovery(new URL(issuerUrl), clientId, clientSecret),
);
}
private resolveRoleFromOidc(userInfo: Record<string, unknown>, config: SSOProviderConfig): 'admin' | 'viewer' {
@@ -568,17 +596,24 @@ export class SSOService {
if (!config) {
return { success: false, error: `Provider ${provider} not configured` };
}
if (provider === 'oidc_github') {
return { success: true, issuer: 'https://github.com (OAuth2, non-standard OIDC)' };
}
const issuerUrl = config.oidcIssuerUrl || WELL_KNOWN_ISSUERS[provider];
if (!issuerUrl) {
return { success: false, error: 'Issuer URL not configured' };
}
if (!config.oidcClientId) {
return { success: false, error: 'Client ID is required to test discovery' };
}
try {
if (provider === 'oidc_github') {
return { success: true, issuer: 'https://github.com (OAuth2, non-standard OIDC)' };
}
const issuerUrl = config.oidcIssuerUrl || WELL_KNOWN_ISSUERS[provider];
if (!issuerUrl) {
return { success: false, error: 'Issuer URL not configured' };
}
const issuer = await Issuer.discover(issuerUrl);
return { success: true, issuer: issuer.metadata.issuer };
const oidcConfig = await discovery(
new URL(issuerUrl),
config.oidcClientId,
config.oidcClientSecret || undefined,
);
return { success: true, issuer: oidcConfig.serverMetadata().issuer };
} catch (err) {
const message = err instanceof Error ? err.message : 'Discovery failed';
return { success: false, error: message };