diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 6508003ab..592c8d1c2 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -21,6 +21,6 @@ jobs: with: comment-summary-in-pr: always fail-on-severity: high - allow-licenses: MIT, MIT-0, Apache-2.0, BSD-3-Clause, BSD-3-Clause-Clear, ISC, BSD-2-Clause, Unlicense, CC0-1.0, 0BSD, X11, MPL-2.0, MPL-1.0, MPL-1.1, MPL-2.0, Zlib + allow-licenses: MIT, MIT-0, Apache-2.0, BSD-3-Clause, BSD-3-Clause-Clear, ISC, BSD-2-Clause, Unlicense, CC0-1.0, 0BSD, X11, MPL-2.0, MPL-1.0, MPL-1.1, MPL-2.0, OFL-1.1, Zlib fail-on-scopes: development, runtime allow-dependencies-licenses: 'pkg:npm/caniuse-lite' diff --git a/config.schema.json b/config.schema.json index 9a385ef1f..3661d7464 100644 --- a/config.schema.json +++ b/config.schema.json @@ -103,6 +103,13 @@ } } }, + "apiAuthentication": { + "description": "List of authentication sources for API endpoints. May be empty, in which case all endpoints are public.", + "type": "array", + "items": { + "$ref": "#/definitions/authentication" + } + }, "tls": { "description": "TLS configuration for secure connections", "type": "object", diff --git a/package-lock.json b/package-lock.json index b2bcd87ec..77bb7ed08 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,13 +33,15 @@ "history": "5.3.0", "isomorphic-git": "^1.27.1", "jsonschema": "^1.4.1", + "jsonwebtoken": "^9.0.2", + "jwk-to-pem": "^2.0.7", "load-plugin": "^6.0.0", "lodash": "^4.17.21", "lusca": "^1.7.0", "moment": "^2.29.4", "mongodb": "^5.0.0", "nodemailer": "^6.6.1", - "openid-client": "^6.2.0", + "openid-client": "^6.3.1", "parse-diff": "^0.11.1", "passport": "^0.7.0", "passport-activedirectory": "^1.0.4", @@ -4822,6 +4824,12 @@ "node": ">=8" } }, + "node_modules/brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==", + "license": "MIT" + }, "node_modules/browser-stdout": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", @@ -4902,6 +4910,12 @@ "node": "*" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -6114,6 +6128,15 @@ "safer-buffer": "^2.1.0" } }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -6126,6 +6149,21 @@ "dev": true, "license": "ISC" }, + "node_modules/elliptic": { + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz", + "integrity": "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==", + "license": "MIT", + "dependencies": { + "bn.js": "^4.11.9", + "brorand": "^1.1.0", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" + } + }, "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", @@ -7959,6 +7997,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, "node_modules/hasha": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.2.tgz", @@ -8031,6 +8079,17 @@ "@babel/runtime": "^7.7.6" } }, + "node_modules/hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", + "license": "MIT", + "dependencies": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, "node_modules/hogan.js": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/hogan.js/-/hogan.js-3.0.2.tgz", @@ -9108,9 +9167,9 @@ } }, "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": "5.10.0", + "resolved": "https://registry.npmjs.org/jose/-/jose-5.10.0.tgz", + "integrity": "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/panva" @@ -9257,6 +9316,40 @@ "node": "*" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/jsprim": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz", @@ -9407,6 +9500,38 @@ "dev": true, "license": "MIT" }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jwk-to-pem": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/jwk-to-pem/-/jwk-to-pem-2.0.7.tgz", + "integrity": "sha512-cSVphrmWr6reVchuKQZdfSs4U9c5Y4hwZggPoz6cbVnTpAVgGRpEuQng86IyqLeGZlhTh+c4MAreB6KbdQDKHQ==", + "license": "Apache-2.0", + "dependencies": { + "asn1.js": "^5.3.0", + "elliptic": "^6.6.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -9633,11 +9758,40 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, "node_modules/lodash.isplainobject": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", - "dev": true + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" }, "node_modules/lodash.kebabcase": { "version": "4.1.1", @@ -9662,8 +9816,7 @@ "node_modules/lodash.once": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", - "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", - "dev": true + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" }, "node_modules/lodash.snakecase": { "version": "4.1.1", @@ -9996,6 +10149,12 @@ "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" }, + "node_modules/minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==", + "license": "MIT" + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -10641,9 +10800,9 @@ } }, "node_modules/oauth4webapi": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-3.2.0.tgz", - "integrity": "sha512-2sYwQXuuzGKOHpnM7QL9BssDrly5gKCgJKTyrhmFIHzJRj0fFsr6GVJEdesmrX6NpMg2u63V4hJwRsZE6PUSSA==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-3.3.0.tgz", + "integrity": "sha512-ZlozhPlFfobzh3hB72gnBFLjXpugl/dljz1fJSRdqaV2r3D5dmi5lg2QWI0LmUYuazmE+b5exsloEv6toUtw9g==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/panva" @@ -10796,13 +10955,13 @@ } }, "node_modules/openid-client": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-6.2.0.tgz", - "integrity": "sha512-pvLVkLcRWNU7YuKKTto376rgL//+rn3ca0XRqsrQVN30lVlpXBPHhSLcGoM/hPbux5p+Ha4tdoz96eEYpyguOQ==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-6.3.1.tgz", + "integrity": "sha512-l+uRCCM+KaGKQmCWjrjlFHXa1husuc72OPCCyGB7VjHeEMVldfwsn4Pfb/5Xk51FRqbRakMbwfIUPiAMQDHaSw==", "license": "MIT", "dependencies": { - "jose": "^5.9.6", - "oauth4webapi": "^3.2.0" + "jose": "^5.10.0", + "oauth4webapi": "^3.3.0" }, "funding": { "url": "https://github.com/sponsors/panva" diff --git a/package.json b/package.json index 969a0a0ab..3d646f772 100644 --- a/package.json +++ b/package.json @@ -58,13 +58,15 @@ "history": "5.3.0", "isomorphic-git": "^1.27.1", "jsonschema": "^1.4.1", + "jsonwebtoken": "^9.0.2", + "jwk-to-pem": "^2.0.7", "load-plugin": "^6.0.0", "lodash": "^4.17.21", "lusca": "^1.7.0", "moment": "^2.29.4", "mongodb": "^5.0.0", "nodemailer": "^6.6.1", - "openid-client": "^6.2.0", + "openid-client": "^6.3.1", "parse-diff": "^0.11.1", "passport": "^0.7.0", "passport-activedirectory": "^1.0.4", diff --git a/proxy.config.json b/proxy.config.json index be7aa2327..618603a6a 100644 --- a/proxy.config.json +++ b/proxy.config.json @@ -146,6 +146,16 @@ "contactEmail": "", "csrfProtection": true, "plugins": [], + "apiAuthentication": [ + { + "type": "jwt", + "enabled": false, + "jwtConfig": { + "clientID": "", + "authorityURL": "" + } + } + ], "tls": { "enabled": false, "key": "certs/key.pem", diff --git a/src/config/index.ts b/src/config/index.ts index 17df09555..63174a296 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -19,6 +19,7 @@ if (existsSync(configFile)) { let _authorisedList: AuthorisedRepo[] = defaultSettings.authorisedList; let _database: Database[] = defaultSettings.sink; let _authentication: Authentication[] = defaultSettings.authentication; +let _apiAuthentication: Authentication[] = defaultSettings.apiAuthentication; let _tempPassword: TempPasswordConfig = defaultSettings.tempPassword; let _proxyUrl = defaultSettings.proxyUrl; let _api: Record = defaultSettings.api; @@ -91,6 +92,8 @@ export const getDatabase = () => { /** * Get the list of enabled authentication methods + * + * At least one authentication method must be enabled. * @return {Array} List of enabled authentication methods */ export const getAuthMethods = () => { @@ -107,6 +110,22 @@ export const getAuthMethods = () => { return enabledAuthMethods; }; +/** + * Get the list of enabled authentication methods for API endpoints + * + * If no API authentication methods are enabled, all endpoints are public. + * @return {Array} List of enabled authentication methods + */ +export const getAPIAuthMethods = () => { + if (_userSettings !== null && _userSettings.apiAuthentication) { + _apiAuthentication = _userSettings.apiAuthentication; + } + + const enabledAuthMethods = _apiAuthentication.filter(auth => auth.enabled); + + return enabledAuthMethods; +}; + // Log configuration to console export const logConfiguration = () => { console.log(`authorisedList = ${JSON.stringify(getAuthorisedList())}`); diff --git a/src/config/types.ts b/src/config/types.ts index a1907477a..291de4081 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -1,9 +1,11 @@ import { Options as RateLimitOptions } from 'express-rate-limit'; export interface UserSettings { + uiRouteAuth: Record; authorisedList: AuthorisedRepo[]; sink: Database[]; authentication: Authentication[]; + apiAuthentication: Authentication[]; tempPassword?: TempPasswordConfig; proxyUrl: string; api: Record; diff --git a/src/service/passport/activeDirectory.js b/src/service/passport/activeDirectory.js index 372868133..eef2f7826 100644 --- a/src/service/passport/activeDirectory.js +++ b/src/service/passport/activeDirectory.js @@ -1,6 +1,8 @@ const ActiveDirectoryStrategy = require('passport-activedirectory'); const ldaphelper = require('./ldaphelper'); +const type = "activedirectory"; + const configure = (passport) => { const db = require('../../db'); @@ -15,6 +17,7 @@ const configure = (passport) => { console.log(`AD User Group: ${userGroup}, AD Admin Group: ${adminGroup}`); passport.use( + type, new ActiveDirectoryStrategy( { passReqToCallback: true, @@ -28,11 +31,6 @@ const configure = (passport) => { profile.id = profile.username; req.user = profile; - console.log( - `passport.activeDirectory: resolved login ${ - profile._json.userPrincipalName - }, profile=${JSON.stringify(profile)}`, - ); // First check to see if the user is in the usergroups const isUser = await ldaphelper.isUserInAdGroup(profile.username, domain, userGroup); @@ -45,7 +43,6 @@ const configure = (passport) => { const isAdmin = await ldaphelper.isUserInAdGroup(profile.username, domain, adminGroup); profile.admin = isAdmin; - console.log(`passport.activeDirectory: ${profile.username} admin=${isAdmin}`); const user = { username: profile.username, @@ -77,4 +74,4 @@ const configure = (passport) => { return passport; }; -module.exports = { configure }; +module.exports = { configure, type }; diff --git a/src/service/passport/index.js b/src/service/passport/index.js index 72918282f..b0712d510 100644 --- a/src/service/passport/index.js +++ b/src/service/passport/index.js @@ -16,6 +16,7 @@ const configure = async () => { passport.initialize(); const authMethods = config.getAuthMethods(); + console.log(`authMethods: ${JSON.stringify(authMethods)}`); for (const auth of authMethods) { const strategy = authStrategies[auth.type.toLowerCase()]; diff --git a/src/service/passport/jwtAuthHandler.js b/src/service/passport/jwtAuthHandler.js new file mode 100644 index 000000000..da9e3bc47 --- /dev/null +++ b/src/service/passport/jwtAuthHandler.js @@ -0,0 +1,106 @@ +const axios = require("axios"); +const jwt = require("jsonwebtoken"); +const jwkToPem = require("jwk-to-pem"); +const config = require('../../config'); + +/** + * Obtain the JSON Web Key Set (JWKS) from the OIDC authority. + * @param {string} authorityUrl the OIDC authority URL. e.g. https://login.microsoftonline.com/{tenantId} + * @return {Promise} the JWKS keys + */ +async function getJwks(authorityUrl) { + try { + const { data } = await axios.get(`${authorityUrl}/.well-known/openid-configuration`); + const jwksUri = data.jwks_uri; + + const { data: jwks } = await axios.get(jwksUri); + return jwks.keys; + } catch (error) { + console.error("Error fetching JWKS:", error); + throw new Error("Failed to fetch JWKS"); + } +} + +/** + * Validate a JWT token using the OIDC configuration. + * @param {*} token the JWT token + * @param {*} authorityUrl the OIDC authority URL + * @param {*} clientID the OIDC client ID + * @param {*} expectedAudience the expected audience for the token + * @return {Promise} the verified payload or an error + */ +async function validateJwt(token, authorityUrl, clientID, expectedAudience) { + try { + const jwks = await getJwks(authorityUrl); + + const decodedHeader = await jwt.decode(token, { complete: true }); + if (!decodedHeader || !decodedHeader.header || !decodedHeader.header.kid) { + throw new Error("Invalid JWT: Missing key ID (kid)"); + } + + const { kid } = decodedHeader.header; + const jwk = jwks.find((key) => key.kid === kid); + if (!jwk) { + throw new Error("No matching key found in JWKS"); + } + + const pubKey = jwkToPem(jwk); + + const verifiedPayload = jwt.verify(token, pubKey, { + algorithms: ["RS256"], + issuer: authorityUrl, + audience: expectedAudience, + }); + + if (verifiedPayload.azp !== clientID) { + throw new Error("JWT client ID does not match"); + } + + return { verifiedPayload }; + } catch (error) { + const errorMessage = `JWT validation failed: ${error.message}\n`; + console.error(errorMessage); + return { error: errorMessage }; + } +} + +const jwtAuthHandler = () => { + return async (req, res, next) => { + const apiAuthMethods = config.getAPIAuthMethods(); + const jwtAuthMethod = apiAuthMethods.find((method) => method.type.toLowerCase() === "jwt"); + if (!jwtAuthMethod) { + return next(); + } + + if (req.isAuthenticated()) { + return next(); + } + + const token = req.header("Authorization"); + if (!token) { + return res.status(401).send("No token provided\n"); + } + + const { clientID, authorityURL, expectedAudience } = jwtAuthMethod.jwtConfig; + const audience = expectedAudience || clientID; + + if (!authorityURL) { + return res.status(500).send("OIDC authority URL is not configured\n"); + } + + if (!clientID) { + return res.status(500).send("OIDC client ID is not configured\n"); + } + + const tokenParts = token.split(" "); + const { verifiedPayload, error } = await validateJwt(tokenParts[1], authorityURL, audience, clientID); + if (error) { + return res.status(401).send(error); + } + + req.user = verifiedPayload; + return next(); + } +} + +module.exports = jwtAuthHandler; diff --git a/src/service/passport/local.js b/src/service/passport/local.js index 8fc0b369c..579d47234 100644 --- a/src/service/passport/local.js +++ b/src/service/passport/local.js @@ -38,7 +38,6 @@ const configure = async (passport) => { } }); - passport.type = 'local'; return passport; }; diff --git a/src/service/passport/oidc.js b/src/service/passport/oidc.js index 4954a4d7b..18fdf7de9 100644 --- a/src/service/passport/oidc.js +++ b/src/service/passport/oidc.js @@ -1,9 +1,8 @@ -const passport = require('passport'); const db = require('../../db'); -let type; +const type = 'openidconnect'; -const configure = async () => { +const configure = async (passport) => { // Temp fix for ERR_REQUIRE_ESM, will be changed when we refactor to ESM const { discovery, fetchUserInfo } = await import('openid-client'); const { Strategy } = await import('openid-client/passport'); @@ -37,7 +36,8 @@ const configure = async () => { return currentUrl; }; - passport.use(strategy); + // Prevent default strategy name from being overridden with the server host + passport.use(type, strategy); passport.serializeUser((user, done) => { done(null, user.oidcId || user.username); @@ -51,23 +51,22 @@ const configure = async () => { done(err); } }) - console.log(`setting type to ${server.host}`) - type = server.host; return passport; } catch (error) { console.error('OIDC configuration failed:', error); throw error; } -}; +} /** * Handles user authentication with OIDC. - * @param {*} userInfo the OIDC user info object - * @param {*} done the callback function - * @return {Promise} a promise with the authenticated user or an error + * @param {Object} userInfo the OIDC user info object + * @param {Function} done the callback function + * @return {Promise} a promise with the authenticated user or an error */ const handleUserAuthentication = async (userInfo, done) => { + console.log('handleUserAuthentication called'); try { const user = await db.findUserByOIDC(userInfo.sub); @@ -113,9 +112,4 @@ const getUsername = (email) => { return email ? email.split('@')[0] : ''; }; -module.exports = { - configure, - get type() { - return type; - } -}; +module.exports = { configure, type }; diff --git a/src/service/routes/index.js b/src/service/routes/index.js index a7529a4ac..45b276c17 100644 --- a/src/service/routes/index.js +++ b/src/service/routes/index.js @@ -6,14 +6,15 @@ const repo = require('./repo'); const users = require('./users'); const healthcheck = require('./healthcheck'); const config = require('./config'); +const jwtAuthHandler = require('../passport/jwtAuthHandler'); const router = new express.Router(); router.use('/api', home); router.use('/api/auth', auth); router.use('/api/v1/healthcheck', healthcheck); -router.use('/api/v1/push', push); -router.use('/api/v1/repo', repo); -router.use('/api/v1/user', users); +router.use('/api/v1/push', jwtAuthHandler(), push); +router.use('/api/v1/repo', jwtAuthHandler(), repo); +router.use('/api/v1/user', jwtAuthHandler(), users); router.use('/api/v1/config', config); module.exports = router; diff --git a/src/ui/auth/AuthProvider.tsx b/src/ui/auth/AuthProvider.tsx index 1da89df51..f7ab4b2fc 100644 --- a/src/ui/auth/AuthProvider.tsx +++ b/src/ui/auth/AuthProvider.tsx @@ -16,13 +16,10 @@ export const AuthProvider = ({ children }) => { const [isLoading, setIsLoading] = useState(true); const refreshUser = async () => { - console.log('Refreshing user'); try { const data = await getUserInfo(); setUser(data); - console.log('User refreshed:', data); } catch (error) { - console.error('Error refreshing user:', error); setUser(null); } finally { setIsLoading(false); diff --git a/src/ui/components/RouteGuard/RouteGuard.tsx b/src/ui/components/RouteGuard/RouteGuard.tsx index 5004360f0..a729b1660 100644 --- a/src/ui/components/RouteGuard/RouteGuard.tsx +++ b/src/ui/components/RouteGuard/RouteGuard.tsx @@ -36,8 +36,6 @@ const RouteGuard = ({ component: Component, fullRoutePath }: RouteGuardProps) => setAdminOnly(adminOnly || rule.adminOnly); } } - } else { - console.log('UI route auth is not enabled.'); } setAuthChecked(true); });