diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b51950c..a553871 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,10 +24,6 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - cache: "npm" - node-version-file: package.json - run: npm ci - name: Cypress run uses: cypress-io/github-action@v6 diff --git a/index.js b/index.js index 0c2fe70..7547668 100644 --- a/index.js +++ b/index.js @@ -1,12 +1,15 @@ import "dotenv/config"; import express from "express"; -import * as client from "openid-client"; +import { Issuer } from "openid-client"; import session from "express-session"; import morgan from "morgan"; +import * as crypto from "crypto"; import bodyParser from "body-parser"; -import { chain, isObject } from "lodash-es"; const port = parseInt(process.env.PORT, 10) || 3000; +const origin = `${process.env.HOST}`; +const redirectUri = `${origin}${process.env.CALLBACK_URL}`; + const app = express(); app.set("view engine", "ejs"); @@ -19,35 +22,31 @@ app.use( ); app.use(morgan("combined")); -const objToUrlParams = (obj) => - new URLSearchParams( - chain(obj) - .omitBy((v) => !v) - .mapValues((o) => (isObject(o) ? JSON.stringify(o) : o)) - .value(), - ); - -const getCurrentUrl = (req) => - new URL(`${req.protocol}://${req.get("host")}${req.originalUrl}`); - -const getProviderConfig = async () => { - return await client.discovery( - new URL(process.env.PC_PROVIDER), - process.env.PC_CLIENT_ID, - { - client_secret: process.env.PC_CLIENT_SECRET, - id_token_signed_response_alg: process.env.PC_ID_TOKEN_SIGNED_RESPONSE_ALG, - userinfo_signed_response_alg: - process.env.PC_USERINFO_SIGNED_RESPONSE_ALG || null, - }, - ); +const removeNullValues = (obj) => Object.entries(obj).reduce((a,[k,v]) => (v ? (a[k]=v, a) : a), {}) + +const getMcpClient = async () => { + const mcpIssuer = await Issuer.discover(process.env.PC_PROVIDER); + + return new mcpIssuer.Client({ + client_id: process.env.PC_CLIENT_ID, + client_secret: process.env.PC_CLIENT_SECRET, + redirect_uris: [redirectUri], + response_types: ["code"], + id_token_signed_response_alg: process.env.PC_ID_TOKEN_SIGNED_RESPONSE_ALG, + userinfo_signed_response_alg: + process.env.PC_USERINFO_SIGNED_RESPONSE_ALG || null, + }); }; +const acr_values = process.env.ACR_VALUES + ? process.env.ACR_VALUES.split(",") + : null; +const login_hint = process.env.LOGIN_HINT || null; +const scope = process.env.PC_SCOPES; const AUTHORIZATION_DEFAULT_PARAMS = { - redirect_uri: `${process.env.HOST}${process.env.CALLBACK_URL}`, - scope: process.env.PC_SCOPES, - login_hint: process.env.LOGIN_HINT || null, - acr_values: process.env.ACR_VALUES ? process.env.ACR_VALUES.split(",") : null, + scope, + login_hint, + acr_values, claims: { id_token: { amr: { @@ -76,22 +75,19 @@ app.get("/", async (req, res, next) => { const getAuthorizationControllerFactory = (extraParams) => { return async (req, res, next) => { try { - const config = await getProviderConfig(); - const nonce = client.randomNonce(); - const state = client.randomState(); + const client = await getMcpClient(); + const nonce = crypto.randomBytes(16).toString("hex"); + const state = crypto.randomBytes(16).toString("hex"); req.session.state = state; req.session.nonce = nonce; - const redirectUrl = client.buildAuthorizationUrl( - config, - objToUrlParams({ - nonce, - state, - ...AUTHORIZATION_DEFAULT_PARAMS, - ...extraParams, - }), - ); + const redirectUrl = client.authorizationUrl(removeNullValues({ + nonce, + state, + ...AUTHORIZATION_DEFAULT_PARAMS, + ...extraParams, + })); res.redirect(redirectUrl); } catch (e) { @@ -147,7 +143,7 @@ app.post( "/custom-connection", bodyParser.urlencoded({ extended: false }), (req, res, next) => { - const customParams = JSON.parse(req.body["custom-params"]); + const customParams = JSON.parse(req.body['custom-params']) return getAuthorizationControllerFactory(customParams)(req, res, next); }, @@ -155,24 +151,19 @@ app.post( app.get(process.env.CALLBACK_URL, async (req, res, next) => { try { - const config = await getProviderConfig(); - const currentUrl = getCurrentUrl(req); - const tokens = await client.authorizationCodeGrant(config, currentUrl, { - expectedNonce: req.session.nonce, - expectedState: req.session.state, + const client = await getMcpClient(); + const params = client.callbackParams(req); + const tokenSet = await client.callback(redirectUri, params, { + nonce: req.session.nonce, + state: req.session.state, }); req.session.nonce = null; req.session.state = null; - const claims = tokens.claims(); - req.session.userinfo = await client.fetchUserInfo( - config, - tokens.access_token, - claims.sub, - ); - req.session.idtoken = claims; - req.session.id_token_hint = tokens.id_token; - req.session.oauth2token = tokens; + req.session.userinfo = await client.userinfo(tokenSet.access_token); + req.session.idtoken = tokenSet.claims(); + req.session.id_token_hint = tokenSet.id_token; + req.session.oauth2token = tokenSet; res.redirect("/"); } catch (e) { next(e); @@ -183,14 +174,11 @@ app.post("/logout", async (req, res, next) => { try { const id_token_hint = req.session.id_token_hint; req.session.destroy(); - const config = await getProviderConfig(); - const redirectUrl = client.buildEndSessionUrl( - config, - objToUrlParams({ - post_logout_redirect_uri: `${process.env.HOST}/`, - id_token_hint, - }), - ); + const client = await getMcpClient(); + const redirectUrl = client.endSessionUrl({ + post_logout_redirect_uri: `${origin}/`, + id_token_hint, + }); res.redirect(redirectUrl); } catch (e) { diff --git a/package-lock.json b/package-lock.json index b4b7530..b240a09 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,9 +14,8 @@ "ejs": "^3.1.10", "express": "^4.21.1", "express-session": "^1.18.1", - "lodash-es": "^4.17.21", "morgan": "^1.10.0", - "openid-client": "^6.1.3" + "openid-client": "^5.7.0" }, "devDependencies": { "prettier": "^3.3.3" @@ -598,17 +597,23 @@ } }, "node_modules/jose": { - "version": "5.9.6", - "resolved": "https://registry.npmjs.org/jose/-/jose-5.9.6.tgz", - "integrity": "sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ==", + "version": "4.15.9", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", + "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==", "funding": { "url": "https://github.com/sponsors/panva" } }, - "node_modules/lodash-es": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", - "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" + "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==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } }, "node_modules/media-typer": { "version": "0.3.0", @@ -714,12 +719,12 @@ "node": ">= 0.6" } }, - "node_modules/oauth4webapi": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-3.1.2.tgz", - "integrity": "sha512-KQZkNU+xn02lWrFu5Vjqg9E81yPtDSxUZorRHlLWVoojD+H/0GFbH59kcnz5Thdjj7c4/mYMBPj/mhvGe/kKXA==", - "funding": { - "url": "https://github.com/sponsors/panva" + "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==", + "engines": { + "node": ">= 6" } }, "node_modules/object-inspect": { @@ -733,6 +738,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/oidc-token-hash": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.3.tgz", + "integrity": "sha512-IF4PcGgzAr6XXSff26Sk/+P4KZFJVuHAJZj3wgO3vX2bMdNVp/QXTP3P7CEm9V1IdG8lDLY3HhiqpsE/nOwpPw==", + "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", @@ -753,12 +766,14 @@ } }, "node_modules/openid-client": { - "version": "6.1.3", - "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-6.1.3.tgz", - "integrity": "sha512-74sc0bR4ptfwCwMheLPaJHTQnds+97Yu6O8eQgoO3MRcd53xkfKyl3gNAsRsYSYoO+AVG3eCgnRMjRkZ6n2RYw==", + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.7.0.tgz", + "integrity": "sha512-4GCCGZt1i2kTHpwvaC/sCpTpQqDnBzDzuJcJMbH+y1Q5qI8U8RBvoSh28svarXszZHR5BAMXbJPX1PGPRE3VOA==", "dependencies": { - "jose": "^5.9.6", - "oauth4webapi": "^3.1.1" + "jose": "^4.15.9", + "lru-cache": "^6.0.0", + "object-hash": "^2.2.0", + "oidc-token-hash": "^5.0.3" }, "funding": { "url": "https://github.com/sponsors/panva" @@ -1033,6 +1048,11 @@ "engines": { "node": ">= 0.8" } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" } }, "dependencies": { @@ -1467,14 +1487,17 @@ } }, "jose": { - "version": "5.9.6", - "resolved": "https://registry.npmjs.org/jose/-/jose-5.9.6.tgz", - "integrity": "sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ==" - }, - "lodash-es": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", - "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" + "version": "4.15.9", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", + "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==" + }, + "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==", + "requires": { + "yallist": "^4.0.0" + } }, "media-typer": { "version": "0.3.0", @@ -1549,16 +1572,21 @@ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" }, - "oauth4webapi": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-3.1.2.tgz", - "integrity": "sha512-KQZkNU+xn02lWrFu5Vjqg9E81yPtDSxUZorRHlLWVoojD+H/0GFbH59kcnz5Thdjj7c4/mYMBPj/mhvGe/kKXA==" + "object-hash": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz", + "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==" }, "object-inspect": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==" }, + "oidc-token-hash": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.3.tgz", + "integrity": "sha512-IF4PcGgzAr6XXSff26Sk/+P4KZFJVuHAJZj3wgO3vX2bMdNVp/QXTP3P7CEm9V1IdG8lDLY3HhiqpsE/nOwpPw==" + }, "on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -1573,12 +1601,14 @@ "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==" }, "openid-client": { - "version": "6.1.3", - "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-6.1.3.tgz", - "integrity": "sha512-74sc0bR4ptfwCwMheLPaJHTQnds+97Yu6O8eQgoO3MRcd53xkfKyl3gNAsRsYSYoO+AVG3eCgnRMjRkZ6n2RYw==", + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.7.0.tgz", + "integrity": "sha512-4GCCGZt1i2kTHpwvaC/sCpTpQqDnBzDzuJcJMbH+y1Q5qI8U8RBvoSh28svarXszZHR5BAMXbJPX1PGPRE3VOA==", "requires": { - "jose": "^5.9.6", - "oauth4webapi": "^3.1.1" + "jose": "^4.15.9", + "lru-cache": "^6.0.0", + "object-hash": "^2.2.0", + "oidc-token-hash": "^5.0.3" } }, "parseurl": { @@ -1766,6 +1796,11 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" } } } diff --git a/package.json b/package.json index 9c794db..b9dda26 100644 --- a/package.json +++ b/package.json @@ -24,9 +24,8 @@ "ejs": "^3.1.10", "express": "^4.21.1", "express-session": "^1.18.1", - "lodash-es": "^4.17.21", "morgan": "^1.10.0", - "openid-client": "^6.1.3" + "openid-client": "^5.7.0" }, "devDependencies": { "prettier": "^3.3.3" diff --git a/views/index.ejs b/views/index.ejs index 6ad19b5..4278b02 100644 --- a/views/index.ejs +++ b/views/index.ejs @@ -101,7 +101,7 @@

La liste des paramètres utilisables est disponible dans la