From 1af8e30e5fbdeb919b6330281ebef9692bab97b8 Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Fri, 13 Jan 2023 17:25:43 -0700 Subject: [PATCH 1/8] support pkce, client side oauth workflows eliminates the need for eas to have direct access to identity provider so as long as the user-agent can communicate with both eas and provider auth can be provided Signed-off-by: Travis Glenn Hansen --- package-lock.json | 127 ++- package.json | 10 +- src/config_token_store/adapter/env/index.js | 5 +- src/config_token_store/adapter/file/index.js | 7 +- src/config_token_store/adapter/sql/index.js | 9 +- src/config_token_store/index.js | 9 +- src/plugin/oauth/index.js | 766 ++++++++++++++++--- src/server.js | 43 +- src/store.js | 3 +- src/utils.js | 83 ++ 10 files changed, 838 insertions(+), 224 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4644197..dd4783a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.1.0", "license": "MIT", "dependencies": { - "@grpc/grpc-js": "^1.3.2", + "@grpc/grpc-js": "^1.8.4", "@grpc/proto-loader": "^0.7.2", "body-parser": "^1.19.0", "bunyan": "^1.8.15", @@ -29,11 +29,11 @@ "handlebars": "^4.7.7", "htpasswd-js": "^1.0.2", "jq.node": "^2.4.0", - "jsonata": "^1.8.4", + "jsonata": "^2.0.1", "jsonpath": "^1.1.0", "jsonwebtoken": "^9.0.0", - "jwks-rsa": "^2.0.2", - "knex": "^2.2.0", + "jwks-rsa": "^3.0.1", + "knex": "^2.4.0", "ldapauth-fork": "^5.0.1", "lodash": "^4.17.21", "lru-cache": "^7.4.0", @@ -45,7 +45,7 @@ "oracle": "^0.4.1", "pg": "^8.5.1", "prom-client": "^14.0.1", - "query-string": "^7.0.0", + "query-string": "^7.1.3", "request": "^2.88.2", "sqlite3": "^5.0.2", "uri-js": "^4.4.1", @@ -430,9 +430,9 @@ "optional": true }, "node_modules/@grpc/grpc-js": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.8.1.tgz", - "integrity": "sha512-mdqYADWl/9Kb75XLstt6pvBnS1DpxSDFRKrbadkY1ymUd29hq49nP6tLcL7a7qLydgqFCpjNwa2RsyrqB3MsXA==", + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.8.4.tgz", + "integrity": "sha512-oaETBotls7FTBpySg5dhyUCyXSxSeCMmkBBXHXG1iw57MiNoB6D7VRhkrXYbwyHM3Q3Afjp4KlsBX0Zb+ELZXw==", "dependencies": { "@grpc/proto-loader": "^0.7.0", "@types/node": ">=12.12.47" @@ -674,14 +674,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@panva/asn1.js": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@panva/asn1.js/-/asn1.js-1.0.0.tgz", - "integrity": "sha512-UdkG3mLEqXgnlKsWanWcgb6dOjUzJ+XC5f+aWw30qrtjxeNUSfKX1cd5FBzOaXQumoe9nIqeZUvrRJS03HCCtw==", - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -876,9 +868,9 @@ } }, "node_modules/@types/jsonwebtoken": { - "version": "8.5.9", - "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-8.5.9.tgz", - "integrity": "sha512-272FMnFGzAVMGtu9tkr29hRL6bZj4Zs1KZNeHLnKqAvp06tAIcarTMwOh8/8bz4FmKRcMxZhZNeUAQsNLoiPhg==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.1.tgz", + "integrity": "sha512-c5ltxazpWabia/4UzhIoaDcIza4KViOQhdbjRlfcIGVnsE3c3brkz9Z+F/EeJIECOQP7W7US2hNE930cWWkPiw==", "dependencies": { "@types/node": "*" } @@ -4121,15 +4113,9 @@ } }, "node_modules/jose": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/jose/-/jose-2.0.6.tgz", - "integrity": "sha512-FVoPY7SflDodE4lknJmbAHSUjLCzE2H1F6MS0RYKMQ8SR+lNccpMf8R4eqkNYyyUjR5qZReOzZo5C5YiHOCjjg==", - "dependencies": { - "@panva/asn1.js": "^1.0.0" - }, - "engines": { - "node": ">=10.13.0 < 13 || >=13.7.0" - }, + "version": "4.11.2", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.11.2.tgz", + "integrity": "sha512-njj0VL2TsIxCtgzhO+9RRobBvws4oYyCM8TpvoUQwl/MbIM3NFJRR9+e6x0sS5xXaP1t6OCBkaBME98OV9zU5A==", "funding": { "url": "https://github.com/sponsors/panva" } @@ -4208,9 +4194,9 @@ } }, "node_modules/jsonata": { - "version": "1.8.6", - "resolved": "https://registry.npmjs.org/jsonata/-/jsonata-1.8.6.tgz", - "integrity": "sha512-ZH2TPYdNP2JecOl/HvrH47Xc+9imibEMQ4YqKy/F/FrM+2a6vfbGxeCX23dB9Fr6uvGwv+ghf1KxWB3iZk09wA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jsonata/-/jsonata-2.0.1.tgz", + "integrity": "sha512-N6VlJPARx9+2+IvjkAuCz7JOh23kgk98EVRE3Xg8RZd9WhwoouR7v2lLpognSlTsMO/652KCGHfIOBn7NGOeyw==", "engines": { "node": ">= 8" } @@ -4329,19 +4315,19 @@ } }, "node_modules/jwks-rsa": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-2.1.5.tgz", - "integrity": "sha512-IODtn1SwEm7n6GQZnQLY0oxKDrMh7n/jRH1MzE8mlxWMrh2NnMyOsXTebu8vJ1qCpmuTJcL4DdiE0E4h8jnwsA==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-3.0.1.tgz", + "integrity": "sha512-UUOZ0CVReK1QVU3rbi9bC7N5/le8ziUj0A2ef1Q0M7OPD2KvjEYizptqIxGIo6fSLYDkqBrazILS18tYuRc8gw==", "dependencies": { "@types/express": "^4.17.14", - "@types/jsonwebtoken": "^8.5.9", + "@types/jsonwebtoken": "^9.0.0", "debug": "^4.3.4", - "jose": "^2.0.6", + "jose": "^4.10.4", "limiter": "^1.1.5", "lru-memoizer": "^2.1.4" }, "engines": { - "node": ">=10 < 13 || >=14" + "node": ">=14" } }, "node_modules/jwks-rsa/node_modules/debug": { @@ -4383,9 +4369,9 @@ } }, "node_modules/knex": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/knex/-/knex-2.3.0.tgz", - "integrity": "sha512-WMizPaq9wRMkfnwKXKXgBZeZFOSHGdtoSz5SaLAVNs3WRDfawt9O89T4XyH52PETxjV8/kRk0Yf+8WBEP/zbYw==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/knex/-/knex-2.4.0.tgz", + "integrity": "sha512-i0GWwqYp1Hs2yvc2rlDO6nzzkLhwdyOZKRdsMTB8ZxOs2IXQyL5rBjSbS1krowCh6V65T4X9CJaKtuIfkaPGSA==", "dependencies": { "colorette": "2.0.19", "commander": "^9.1.0", @@ -5598,14 +5584,6 @@ "url": "https://github.com/sponsors/panva" } }, - "node_modules/openid-client/node_modules/jose": { - "version": "4.11.2", - "resolved": "https://registry.npmjs.org/jose/-/jose-4.11.2.tgz", - "integrity": "sha512-njj0VL2TsIxCtgzhO+9RRobBvws4oYyCM8TpvoUQwl/MbIM3NFJRR9+e6x0sS5xXaP1t6OCBkaBME98OV9zU5A==", - "funding": { - "url": "https://github.com/sponsors/panva" - } - }, "node_modules/openid-client/node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -8006,9 +7984,9 @@ "optional": true }, "@grpc/grpc-js": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.8.1.tgz", - "integrity": "sha512-mdqYADWl/9Kb75XLstt6pvBnS1DpxSDFRKrbadkY1ymUd29hq49nP6tLcL7a7qLydgqFCpjNwa2RsyrqB3MsXA==", + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.8.4.tgz", + "integrity": "sha512-oaETBotls7FTBpySg5dhyUCyXSxSeCMmkBBXHXG1iw57MiNoB6D7VRhkrXYbwyHM3Q3Afjp4KlsBX0Zb+ELZXw==", "requires": { "@grpc/proto-loader": "^0.7.0", "@types/node": ">=12.12.47" @@ -8185,11 +8163,6 @@ } } }, - "@panva/asn1.js": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@panva/asn1.js/-/asn1.js-1.0.0.tgz", - "integrity": "sha512-UdkG3mLEqXgnlKsWanWcgb6dOjUzJ+XC5f+aWw30qrtjxeNUSfKX1cd5FBzOaXQumoe9nIqeZUvrRJS03HCCtw==" - }, "@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -8364,9 +8337,9 @@ } }, "@types/jsonwebtoken": { - "version": "8.5.9", - "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-8.5.9.tgz", - "integrity": "sha512-272FMnFGzAVMGtu9tkr29hRL6bZj4Zs1KZNeHLnKqAvp06tAIcarTMwOh8/8bz4FmKRcMxZhZNeUAQsNLoiPhg==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.1.tgz", + "integrity": "sha512-c5ltxazpWabia/4UzhIoaDcIza4KViOQhdbjRlfcIGVnsE3c3brkz9Z+F/EeJIECOQP7W7US2hNE930cWWkPiw==", "requires": { "@types/node": "*" } @@ -10846,12 +10819,9 @@ } }, "jose": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/jose/-/jose-2.0.6.tgz", - "integrity": "sha512-FVoPY7SflDodE4lknJmbAHSUjLCzE2H1F6MS0RYKMQ8SR+lNccpMf8R4eqkNYyyUjR5qZReOzZo5C5YiHOCjjg==", - "requires": { - "@panva/asn1.js": "^1.0.0" - } + "version": "4.11.2", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.11.2.tgz", + "integrity": "sha512-njj0VL2TsIxCtgzhO+9RRobBvws4oYyCM8TpvoUQwl/MbIM3NFJRR9+e6x0sS5xXaP1t6OCBkaBME98OV9zU5A==" }, "jq.node": { "version": "2.4.0", @@ -10913,9 +10883,9 @@ } }, "jsonata": { - "version": "1.8.6", - "resolved": "https://registry.npmjs.org/jsonata/-/jsonata-1.8.6.tgz", - "integrity": "sha512-ZH2TPYdNP2JecOl/HvrH47Xc+9imibEMQ4YqKy/F/FrM+2a6vfbGxeCX23dB9Fr6uvGwv+ghf1KxWB3iZk09wA==" + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jsonata/-/jsonata-2.0.1.tgz", + "integrity": "sha512-N6VlJPARx9+2+IvjkAuCz7JOh23kgk98EVRE3Xg8RZd9WhwoouR7v2lLpognSlTsMO/652KCGHfIOBn7NGOeyw==" }, "jsonfile": { "version": "4.0.0", @@ -11013,14 +10983,14 @@ } }, "jwks-rsa": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-2.1.5.tgz", - "integrity": "sha512-IODtn1SwEm7n6GQZnQLY0oxKDrMh7n/jRH1MzE8mlxWMrh2NnMyOsXTebu8vJ1qCpmuTJcL4DdiE0E4h8jnwsA==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-3.0.1.tgz", + "integrity": "sha512-UUOZ0CVReK1QVU3rbi9bC7N5/le8ziUj0A2ef1Q0M7OPD2KvjEYizptqIxGIo6fSLYDkqBrazILS18tYuRc8gw==", "requires": { "@types/express": "^4.17.14", - "@types/jsonwebtoken": "^8.5.9", + "@types/jsonwebtoken": "^9.0.0", "debug": "^4.3.4", - "jose": "^2.0.6", + "jose": "^4.10.4", "limiter": "^1.1.5", "lru-memoizer": "^2.1.4" }, @@ -11058,9 +11028,9 @@ } }, "knex": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/knex/-/knex-2.3.0.tgz", - "integrity": "sha512-WMizPaq9wRMkfnwKXKXgBZeZFOSHGdtoSz5SaLAVNs3WRDfawt9O89T4XyH52PETxjV8/kRk0Yf+8WBEP/zbYw==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/knex/-/knex-2.4.0.tgz", + "integrity": "sha512-i0GWwqYp1Hs2yvc2rlDO6nzzkLhwdyOZKRdsMTB8ZxOs2IXQyL5rBjSbS1krowCh6V65T4X9CJaKtuIfkaPGSA==", "requires": { "colorette": "2.0.19", "commander": "^9.1.0", @@ -11972,11 +11942,6 @@ "oidc-token-hash": "^5.0.1" }, "dependencies": { - "jose": { - "version": "4.11.2", - "resolved": "https://registry.npmjs.org/jose/-/jose-4.11.2.tgz", - "integrity": "sha512-njj0VL2TsIxCtgzhO+9RRobBvws4oYyCM8TpvoUQwl/MbIM3NFJRR9+e6x0sS5xXaP1t6OCBkaBME98OV9zU5A==" - }, "lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", diff --git a/package.json b/package.json index 72e8668..a001924 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "homepage": "https://github.com/travisghansen/external-auth-server#readme", "license": "MIT", "dependencies": { - "@grpc/grpc-js": "^1.3.2", + "@grpc/grpc-js": "^1.8.4", "@grpc/proto-loader": "^0.7.2", "body-parser": "^1.19.0", "bunyan": "^1.8.15", @@ -36,11 +36,11 @@ "handlebars": "^4.7.7", "htpasswd-js": "^1.0.2", "jq.node": "^2.4.0", - "jsonata": "^1.8.4", + "jsonata": "^2.0.1", "jsonpath": "^1.1.0", "jsonwebtoken": "^9.0.0", - "jwks-rsa": "^2.0.2", - "knex": "^2.2.0", + "jwks-rsa": "^3.0.1", + "knex": "^2.4.0", "ldapauth-fork": "^5.0.1", "lodash": "^4.17.21", "lru-cache": "^7.4.0", @@ -52,7 +52,7 @@ "oracle": "^0.4.1", "pg": "^8.5.1", "prom-client": "^14.0.1", - "query-string": "^7.0.0", + "query-string": "^7.1.3", "request": "^2.88.2", "sqlite3": "^5.0.2", "uri-js": "^4.4.1", diff --git a/src/config_token_store/adapter/env/index.js b/src/config_token_store/adapter/env/index.js index e651168..43953e7 100644 --- a/src/config_token_store/adapter/env/index.js +++ b/src/config_token_store/adapter/env/index.js @@ -1,4 +1,5 @@ const { BaseConfigTokenStoreAdapter } = require(".."); +const YAML = require("yaml"); class EnvConfigTokenStoreAdapter extends BaseConfigTokenStoreAdapter { /** @@ -22,11 +23,11 @@ class EnvConfigTokenStoreAdapter extends BaseConfigTokenStoreAdapter { try { let data = process.env[adapter.config.options.var]; - data = JSON.parse(data); + data = YAML.parse(data); let token; token = data[id]; - return token; + return JSON.stringify(token); } catch (e) { throw e; } diff --git a/src/config_token_store/adapter/file/index.js b/src/config_token_store/adapter/file/index.js index 67e6469..e166811 100644 --- a/src/config_token_store/adapter/file/index.js +++ b/src/config_token_store/adapter/file/index.js @@ -1,5 +1,6 @@ const fs = require("fs"); const { BaseConfigTokenStoreAdapter } = require(".."); +const YAML = require("yaml"); class FileConfigTokenStoreAdapter extends BaseConfigTokenStoreAdapter { /** @@ -23,11 +24,11 @@ class FileConfigTokenStoreAdapter extends BaseConfigTokenStoreAdapter { try { let data = fs.readFileSync(adapter.config.options.path, "utf8"); - data = JSON.parse(data); + data = YAML.parse(data); let token; token = data[id]; - return token; + return JSON.stringify(token); } catch (e) { throw e; } @@ -35,5 +36,5 @@ class FileConfigTokenStoreAdapter extends BaseConfigTokenStoreAdapter { } module.exports = { - FileConfigTokenStoreAdapter + FileConfigTokenStoreAdapter, }; diff --git a/src/config_token_store/adapter/sql/index.js b/src/config_token_store/adapter/sql/index.js index a4349f8..f31d2ed 100644 --- a/src/config_token_store/adapter/sql/index.js +++ b/src/config_token_store/adapter/sql/index.js @@ -46,16 +46,17 @@ class SqlConfigTokenStoreAdapter extends BaseConfigTokenStoreAdapter { try { const resp = await client.raw(adapter.config.options.query, [id]); - + let row; switch (adapter.config.options.config.client) { case "pg": - row = resp.rows[0]; break; + row = resp.rows[0]; + break; default: row = resp[0][0]; } - + return row.token; } catch (e) { throw e; @@ -64,5 +65,5 @@ class SqlConfigTokenStoreAdapter extends BaseConfigTokenStoreAdapter { } module.exports = { - SqlConfigTokenStoreAdapter + SqlConfigTokenStoreAdapter, }; diff --git a/src/config_token_store/index.js b/src/config_token_store/index.js index 2920310..f39b8e9 100644 --- a/src/config_token_store/index.js +++ b/src/config_token_store/index.js @@ -3,12 +3,13 @@ const { EnvConfigTokenStoreAdapter } = require("./adapter/env"); const { FileConfigTokenStoreAdapter } = require("./adapter/file"); const { FileDevConfigTokenStoreAdapter } = require("./adapter/file_dev"); const { SqlConfigTokenStoreAdapter } = require("./adapter/sql"); +const YAML = require("yaml"); const TOKEN_CACHE_PREFIX = "token_store:"; let config_token_stores = process.env["EAS_CONFIG_TOKEN_STORES"]; if (config_token_stores) { - config_token_stores = JSON.parse(config_token_stores); + config_token_stores = YAML.parse(config_token_stores); } else { config_token_stores = {}; } @@ -37,12 +38,6 @@ class ConfigTokenStoreManager { configTokenStoreConfig ); break; - case "file-dev": - configTokenStoreAdapter = new FileDevConfigTokenStoreAdapter( - manager.server, - configTokenStoreConfig - ); - break; case "sql": configTokenStoreAdapter = new SqlConfigTokenStoreAdapter( manager.server, diff --git a/src/plugin/oauth/index.js b/src/plugin/oauth/index.js index bac9b55..bee794c 100644 --- a/src/plugin/oauth/index.js +++ b/src/plugin/oauth/index.js @@ -1,12 +1,16 @@ +const _ = require("lodash"); +const crypto = require("crypto"); const { Assertion } = require("../../assertion"); const { BasePlugin } = require("../../plugin"); const Handlebars = require("handlebars"); -const { Issuer, custom } = require("openid-client"); +const { Issuer, custom, TokenSet } = require("openid-client"); const jwksClient = require("jwks-rsa"); const jwt = require("jsonwebtoken"); const queryString = require("query-string"); const request = require("request"); const URI = require("uri-js"); +const utils = require("../../utils"); +const { v4: uuidv4 } = require("uuid"); custom.setHttpOptionsDefaults({ followRedirect: false, @@ -63,6 +67,9 @@ const issuer_sign_secret = process.env.EAS_ISSUER_SIGN_SECRET || exit_failure("missing EAS_ISSUER_SIGN_SECRET env variable"); +const PLUGIN_STRATEGY_OAUTH = "oauth"; +const PLUGIN_STRATEGY_OIDC = "oidc"; + const SESSION_CACHE_PREFIX = "session:oauth:"; const INTROSPECTION_CACHE_PREFIX = "introspection:oauth:"; const DEFAULT_COOKIE_NAME = "_eas_oauth_session"; @@ -80,6 +87,9 @@ const STATE_CACHE_EXPIRY = "43200"; //12 hours const BACKCHANNEL_LOGOUT_CACHE_PREFIX = "bachchannel_logout:oauth:"; const BACKCHANNEL_LOGOUT_DEFAULT_TTL = 2678400; //31 days +const DEFAULT_NONCE_TTL = 600; +const NONCE_CACHE_PREFIX = "nonce:oauth:"; + let initialized = false; /** @@ -496,10 +506,107 @@ function handlebars_template_properties(src, data) { return ret; } +function sha256(input) { + if (!Buffer.isBuffer(input)) { + input = Buffer.from(input, "utf8"); + } + return crypto.createHash("sha256").update(input).digest(); +} + +function base64URLEncode(str) { + return str + .toString("base64") + .replace(/\+/g, "-") + .replace(/\//g, "_") + .replace(/=/g, ""); +} + +// https://auth0.com/docs/get-started/authentication-and-authorization-flow/call-your-api-using-the-authorization-code-flow-with-pkce +function create_code_challenge(verifier, code_challenge_method = "S256") { + switch (code_challenge_method.toLowerCase()) { + case "plain": + return verifier; + case "s256": + default: + return base64URLEncode(sha256(verifier)); + } +} + +// https://www.authlete.com/developers/pkce/ +function create_code_verifier() { + // between 43 and 128 + return base64URLEncode(crypto.randomBytes(32)); +} + +class StoreHelper { + constructor(server) { + this.server = server; + } + + async save(log_name, CACHE_KEY, payload, ttl = null) { + const helper = this; + const store = helper.server.store; + + helper.server.logger.verbose(`saving ${log_name}: %s`, CACHE_KEY); + + await store.set( + CACHE_KEY, + helper.server.utils.encrypt( + helper.server.secrets.session_encrypt_secret, + JSON.stringify(payload) + ), + ttl + ); + } + + async get(log_name, CACHE_KEY) { + const helper = this; + const store = helper.server.store; + + helper.server.logger.verbose(`retrieving ${log_name}: %s`, CACHE_KEY); + + const encryptedPayload = await store.get(CACHE_KEY); + + helper.server.logger.verbose( + `retrieved encrypted ${log_name} content: %s`, + encryptedPayload + ); + + if (!encryptedPayload) { + helper.server.logger.verbose(`failed to decrypt ${log_name}`); + return false; + } + + let payload = helper.server.utils.decrypt( + helper.server.secrets.session_encrypt_secret, + encryptedPayload + ); + helper.server.logger.debug(`${log_name} data: %s`, payload); + payload = JSON.parse(payload); + + return payload; + } + + async delete(log_name, CACHE_KEY) { + const helper = this; + const store = helper.server.store; + + helper.server.logger.verbose(`deleting ${log_name}: %s`, CACHE_KEY); + + if (CACHE_KEY) { + await store.del(CACHE_KEY); + } + } +} + +let STORE_HELPER; + class BaseOauthPlugin extends BasePlugin { static initialize(server) { if (!initialized) { - server.WebServer.get("/oauth/callback", (req, res) => { + STORE_HELPER = new StoreHelper(server); + + server.WebServer.get("/oauth/callback", async (req, res) => { server.logger.silly("%j", { headers: req.headers, body: req.body, @@ -512,6 +619,11 @@ class BaseOauthPlugin extends BasePlugin { "hex" ); state = jwt.verify(state, issuer_sign_secret); + state = await STORE_HELPER.get( + "state", + STATE_CACHE_PREFIX + state.state_id + ); + const state_redirect_uri = state.request_uri; const parsedStateRedirectURI = URI.parse(state_redirect_uri); @@ -543,6 +655,231 @@ class BaseOauthPlugin extends BasePlugin { } }); + /** + * NOTE: this has the same security risks as the implicit flow + * ie: URLs with codes/tokens in them being captured by browser history/plugins + * this should effectively never be used in lieu of the /oauth/callback-ua-client-code endpoint + */ + server.WebServer.get("/oauth/callback-ua-redirect", (req, res) => { + server.logger.silly("%j", { + headers: req.headers, + body: req.body, + }); + + res.send(` + + + + + + + +`); + }); + + /** + * multistep process + * 1. send all provided params from the authorization response (ie: query or fragment params) to eas for it to formulate a token endpoint for trading `code` for a tokenset + * 2. using the URL from step 1 POST the data to the token endpoint of the provider to retrieve a tokenset in return + * 3. using the tokenset returned from step 2 submit it to eas for storage (and later session creation) + * 4. redirect browser to standard eas callback url (for session creation, access, etc) + */ + server.WebServer.get("/oauth/callback-ua-client-code", (req, res) => { + server.logger.silly("%j", { + headers: req.headers, + body: req.body, + }); + + res.send(` + + + + + + + +`); + }); + + /** + * submit encrypted state and get the issuer token url in return + * + */ + server.WebServer.post("/oauth/client-token-url", async (req, res) => { + server.logger.silly("%j", { + headers: req.headers, + body: req.body, + }); + + try { + let state = server.utils.decrypt( + issuer_encrypt_secret, + req.body.state, + "hex" + ); + state = jwt.verify(state, issuer_sign_secret); + state = await STORE_HELPER.get( + "state", + STATE_CACHE_PREFIX + state.state_id + ); + + const params = req.body; + params["grant_type"] = "authorization_code"; + params["client_id"] = state.client.client_id; + params["redirect_uri"] = state.authorization_redirect_uri; + if (state.nonce) { + params["nonce"] = state.nonce; + } + + if (state.code_verifier) { + params["code_verifier"] = state.code_verifier; + } + + const queryString = Object.keys(params) + .map((key) => { + return ( + encodeURIComponent(key) + "=" + encodeURIComponent(params[key]) + ); + }) + .join("&"); + + res.json({ url: state.issuer.token_endpoint + "?" + queryString }); + } catch (e) { + res.statusCode = 401; + res.end(); + } + }); + + /** + * used to submit a tokenSet associated with a particlar nonce temporarily + * until it can be validated and a session created + */ + server.WebServer.post("/oauth/client-tokenset", async (req, res) => { + server.logger.silly("%j", { + headers: req.headers, + body: req.body, + }); + + let state = server.utils.decrypt( + issuer_encrypt_secret, + req.query.state, + "hex" + ); + state = jwt.verify(state, issuer_sign_secret); + state = await STORE_HELPER.get( + "state", + STATE_CACHE_PREFIX + state.state_id + ); + + let nonceData; + if (state.nonce) { + nonceData = await STORE_HELPER.get( + "nonce", + NONCE_CACHE_PREFIX + state.nonce + ); + } + + // prevent any kind of replay + if (!nonceData || (nonceData && nonceData.tokenSet)) { + res.statusCode = 503; + res.end(); + return; + } + + nonceData.tokenSet = req.body; + await STORE_HELPER.save( + "nonce", + NONCE_CACHE_PREFIX + state.nonce, + nonceData, + state.nonce_ttl + ); + + res.statusCode = 200; + res.end(); + }); + server.WebServer.get("/oauth/end-session-redirect", (req, res) => { server.logger.silly("%j", { headers: req.headers, @@ -812,6 +1149,11 @@ class BaseOauthPlugin extends BasePlugin { } } + get_plugin_strategy() { + const plugin = this; + return plugin.constructor.name == "OpenIdConnectPlugin" ? "oidc" : "oauth2"; + } + /** * Verify the request * @@ -824,10 +1166,7 @@ class BaseOauthPlugin extends BasePlugin { const store = plugin.server.store; const issuer = await plugin.get_issuer(); const client = await plugin.get_client(); - const pluginStrategy = - plugin.constructor.name == "OpenIdConnectPlugin" ? "oidc" : "oauth2"; - const PLUGIN_STRATEGY_OAUTH = "oauth"; - const PLUGIN_STRATEGY_OIDC = "oidc"; + const pluginStrategy = plugin.get_plugin_strategy(); /** * reconstruct original request info from headers etc @@ -870,7 +1209,38 @@ class BaseOauthPlugin extends BasePlugin { authorization_redirect_uri ); - const payload = { + // implicit flow or pkce with fragment requires nonce + let nonce; + if ( + pluginStrategy == PLUGIN_STRATEGY_OIDC && + _.get(plugin.config, "nonce.enabled", false) + ) { + nonce = uuidv4(); + } + + let code_verifier; + let code_challenge; + let code_challenge_method; + // TODO: is pkce limited to oidc? + if (_.get(plugin.config, "pkce.enabled", false)) { + code_verifier = create_code_verifier(); + code_challenge_method = _.get( + plugin.config, + "pkce.code_challenge_method", + "S256" + ); + code_challenge = create_code_challenge( + code_verifier, + code_challenge_method + ); + } + + const state_id = uuidv4(); + const statePointerPayload = { + state_id, + }; + + const statePayload = { request_uri: parentReqInfo.uri, aud: configAudMD5, csrf: plugin.server.utils.generate_csrf_id(), @@ -880,20 +1250,35 @@ class BaseOauthPlugin extends BasePlugin { }, }, request_is_xhr, + nonce, + nonce_ttl: _.get(plugin.config, "nonce.ttl", DEFAULT_NONCE_TTL), + code_verifier, + issuer: { + token_endpoint: issuer.token_endpoint, // add this in case of client-side token retrieval + }, + client: { + client_id: client.client_id, + }, + authorization_redirect_uri, }; - const stateToken = jwt.sign(payload, issuer_sign_secret); + + // persist state + await plugin.save_state(state_id, statePayload); + + const stateToken = jwt.sign(statePointerPayload, issuer_sign_secret); const state = plugin.server.utils.encrypt( issuer_encrypt_secret, stateToken, "hex" ); - const url = await plugin.get_authorization_url( - req, - parentReqInfo, - authorization_redirect_uri, - state - ); + const url = await plugin.get_authorization_url(req, parentReqInfo, { + redirect_uri: authorization_redirect_uri, + state, + nonce, + code_challenge, + code_challenge_method, + }); plugin.server.logger.verbose("callback redirect_uri: %s", url); @@ -901,11 +1286,7 @@ class BaseOauthPlugin extends BasePlugin { case 401: res.setHeader( "WWW-Authenticate", - 'Bearer realm="' + - url + - '", scope="' + - plugin.config.scopes.join(" ") + - '"' + 'Bearer realm="' + url + '", scope="' + plugin.get_scope() + '"' ); default: if (plugin.config.csrf_cookie.enabled) { @@ -913,7 +1294,7 @@ class BaseOauthPlugin extends BasePlugin { STATE_CSRF_COOKIE_NAME, plugin.server.utils.encrypt( plugin.server.secrets.cookie_encrypt_secret, - payload.csrf + statePayload.csrf ), { /** @@ -1088,6 +1469,7 @@ class BaseOauthPlugin extends BasePlugin { const redirectHttpCode = req.query.redirect_http_code ? req.query.redirect_http_code : 302; + plugin.server.logger.verbose("decoded state: %j", state); const configAudMD5 = configToken.audMD5; @@ -1122,17 +1504,9 @@ class BaseOauthPlugin extends BasePlugin { } } - plugin.server.logger.verbose("begin token fetch with authorization code"); - - const compare_redirect_uri = plugin.get_authorization_redirect_uri( - state.request_uri - ); - plugin.server.logger.verbose( - "compare_redirect_uri: %s", - compare_redirect_uri - ); - + let tokenSet; let realRedirectUri; + let clientProvidedTokenSet = false; if ( plugin.config.xhr.use_referer_as_redirect_uri && state.request_is_xhr && @@ -1143,29 +1517,71 @@ class BaseOauthPlugin extends BasePlugin { realRedirectUri = state.request_uri; } - let tokenSet; - try { - tokenSet = await plugin.authorization_code_callback( - req, - parentReqInfo, - compare_redirect_uri - ); - } catch (e) { - plugin.server.logger.verbose("failed to retrieve tokens"); - plugin.server.logger.error(e); - if (plugin.is_redirectable_error(e)) { + if (parentReqInfo.parsedQuery.error) { + plugin.server.logger.verbose("failed to authenticate"); + plugin.server.logger.error(parentReqInfo.parsedQuery); + if (plugin.is_redirectable_error(parentReqInfo.parsedQuery)) { res.statusCode = redirectHttpCode; res.setHeader("Location", realRedirectUri); return res; } - if (plugin.is_unauthorized_error(e)) { + if (plugin.is_unauthorized_error(parentReqInfo.parsedQuery)) { res.statusCode = 403; return res; } res.statusCode = 503; return res; + } else if (parentReqInfo.parsedQuery.code) { + // authorization_code flow + plugin.server.logger.verbose( + "begin token fetch with authorization code" + ); + + const compare_redirect_uri = plugin.get_authorization_redirect_uri( + state.request_uri + ); + plugin.server.logger.verbose( + "compare_redirect_uri: %s", + compare_redirect_uri + ); + + try { + tokenSet = await plugin.authorization_code_callback( + req, + parentReqInfo, + compare_redirect_uri, + state.nonce, + state.code_verifier + ); + } catch (e) { + plugin.server.logger.verbose("failed to retrieve tokens"); + plugin.server.logger.error(e); + if (plugin.is_redirectable_error(e)) { + res.statusCode = redirectHttpCode; + res.setHeader("Location", realRedirectUri); + return res; + } + + if (plugin.is_unauthorized_error(e)) { + res.statusCode = 403; + return res; + } + + res.statusCode = 503; + return res; + } + } else { + // client side tokens retrieved + let nonceData = await plugin.get_nonce(state.nonce); + if (nonceData.tokenSet) { + tokenSet = new TokenSet(nonceData.tokenSet); + clientProvidedTokenSet = true; + } else { + res.statusCode = 503; + return res; + } } plugin.server.logger.verbose("received and validated tokens"); @@ -1177,6 +1593,13 @@ class BaseOauthPlugin extends BasePlugin { return res; } + // TODO: must verify the id_token here as it cannot be trusted otherwise + // currently this is left to the user by enabling assertions.id_token_signature + if (clientProvidedTokenSet && !tokenSet.id_token) { + //jwt.verify(tokenSet.id_token, "foo"); + // TODO: in case of failure return what statusCode? 403 + } + /** * only id_token is guaranteed to be a jwt */ @@ -1186,6 +1609,32 @@ class BaseOauthPlugin extends BasePlugin { idToken = jwt.decode(tokenSet.id_token); } + let nonceData; + if ( + pluginStrategy == PLUGIN_STRATEGY_OIDC && + _.get(plugin.config, "nonce.enabled", false) + ) { + if (!idToken.nonce) { + plugin.server.logger.verbose("missing nonce claim"); + res.statusCode = 503; + return res; + } else { + nonceData = await plugin.get_nonce(idToken.nonce); + if (!nonceData) { + plugin.server.logger.verbose( + "missing nonce data, expired or invalid nonce" + ); + res.statusCode = 503; + return res; + } + } + } + + // failsafe delete regardless of settings + if (idToken && idToken.nonce) { + await plugin.delete_nonce(idToken.nonce); + } + //TODO: see if expires_at is access_token or refresh_token, adjust logic accordingly let cookieExpiresAt, sessionExpiresAt, tokenExpiresAt; if (tokenSet.expires_at) { @@ -1226,6 +1675,7 @@ class BaseOauthPlugin extends BasePlugin { iat: Math.floor(Date.now() / 1000), tokenSet, aud: configAudMD5, + clientProvidedTokenSet, }; let userinfo; @@ -1237,9 +1687,12 @@ class BaseOauthPlugin extends BasePlugin { } } - if (plugin.config.assertions.userinfo) { + if ( + plugin.config.assertions.userinfo && + plugin.config.assertions.userinfo.length > 0 + ) { const userinfoValid = await plugin.userinfo_assertions( - sessionPayload.userinfo.data + _.get(sessionPayload, "userinfo.data") ); if (!userinfoValid) { res.statusCode = 403; @@ -1336,20 +1789,40 @@ class BaseOauthPlugin extends BasePlugin { case "logout": return handle_logout_callback_request(req, res, parentReqInfo); case "authorization_callback": - const state = plugin.server.utils.decrypt( + const statePointerToken = plugin.server.utils.decrypt( issuer_encrypt_secret, parentReqInfo.parsedQuery.state, "hex" ); - const decodedState = jwt.verify(state, issuer_sign_secret); - return handle_auth_callback_request( + const decodedStatePointer = jwt.verify( + statePointerToken, + issuer_sign_secret + ); + plugin.server.logger.verbose( + "decocded state pointer: %s", + JSON.stringify(decodedStatePointer) + ); + const statePayload = await plugin.get_state( + decodedStatePointer.state_id + ); + + let returnPromise = handle_auth_callback_request( configToken, req, res, - decodedState, + statePayload, parentReqInfo ); + // TODO: maybe only delete this after success? + returnPromise.finally(async () => { + try { + await plugin.delete_state(decodedStatePointer.state_id); + } catch (err) {} + }); + + return returnPromise; + case "verify": default: /** @@ -1536,9 +2009,12 @@ class BaseOauthPlugin extends BasePlugin { sessionPayload.userinfo = userinfo; } - if (plugin.config.assertions.userinfo) { + if ( + plugin.config.assertions.userinfo && + plugin.config.assertions.userinfo.length > 0 + ) { const userinfoValid = await plugin.userinfo_assertions( - sessionPayload.userinfo.data + _.get(sessionPayload, "userinfo.data") ); if (!userinfoValid) { return respond_to_failed_authorization(); @@ -1612,9 +2088,12 @@ class BaseOauthPlugin extends BasePlugin { } // run assertions on userinfo - if (plugin.config.assertions.userinfo) { + if ( + plugin.config.assertions.userinfo && + plugin.config.assertions.userinfo.length > 0 + ) { const userinfoValid = await plugin.userinfo_assertions( - sessionPayload.userinfo.data + _.get(sessionPayload, "userinfo.data") ); if (!userinfoValid) { plugin.server.logger.verbose("userinfo failed assertions"); @@ -1854,52 +2333,46 @@ class BaseOauthPlugin extends BasePlugin { return introspectionPayload; } - async get_state(state_id) { - const plugin = this; - const store = plugin.server.store; - plugin.server.logger.verbose("retrieving state: %s", state_id); - - const encryptedState = await store.get(STATE_CACHE_PREFIX + state_id); - - plugin.server.logger.verbose( - "retrieved encrypted state content: %s", - encryptedState - ); - - if (!encryptedState) { - plugin.server.logger.verbose("failed to decrypt state"); - return false; + async save_state(state_id, payload, ttl = null) { + if (!ttl) { + ttl = STATE_CACHE_EXPIRY; } - let statePayload = plugin.server.utils.decrypt( - plugin.server.secrets.session_encrypt_secret, - encryptedState + return STORE_HELPER.save( + "state", + STATE_CACHE_PREFIX + state_id, + payload, + ttl ); - plugin.server.logger.debug("state data: %s", statePayload); - statePayload = JSON.parse(statePayload); + } - return statePayload; + async get_state(state_id) { + return STORE_HELPER.get("state", STATE_CACHE_PREFIX + state_id); } - async save_state(state_id, payload, ttl = null) { + async delete_state(state_id) { + if (state_id) { + await STORE_HELPER.delete("state", STATE_CACHE_PREFIX + state_id); + } + } + + async save_nonce(nonce, payload, ttl = null) { const plugin = this; - const store = plugin.server.store; - await store.set( - STATE_CACHE_PREFIX + state_id, - plugin.server.utils.encrypt( - plugin.server.secrets.session_encrypt_secret, - JSON.stringify(payload) - ), - ttl - ); + if (!ttl) { + ttl = _.get(plugin.config, "nonce.ttl", DEFAULT_NONCE_TTL); + } + + return STORE_HELPER.save("nonce", NONCE_CACHE_PREFIX + nonce, payload, ttl); } - async delete_state(state_id) { - const plugin = this; - const store = plugin.server.store; - if (state_id) { - await store.del(STATE_CACHE_PREFIX + state_id); + async get_nonce(nonce) { + return STORE_HELPER.get("nonce", NONCE_CACHE_PREFIX + nonce); + } + + async delete_nonce(nonce) { + if (nonce) { + await STORE_HELPER.delete("nonce", NONCE_CACHE_PREFIX + nonce); } } @@ -2065,10 +2538,7 @@ class BaseOauthPlugin extends BasePlugin { const issuer = await plugin.get_issuer(); const client = await plugin.get_client(); - const pluginStrategy = - plugin.constructor.name == "OpenIdConnectPlugin" ? "oidc" : "oauth2"; - const PLUGIN_STRATEGY_OAUTH = "oauth"; - const PLUGIN_STRATEGY_OIDC = "oidc"; + const pluginStrategy = plugin.get_plugin_strategy(); /** * token aud is the client_id @@ -2170,7 +2640,8 @@ class BaseOauthPlugin extends BasePlugin { if ( pluginStrategy == PLUGIN_STRATEGY_OIDC && - plugin.config.assertions.id_token + plugin.config.assertions.id_token && + plugin.config.assertions.id_token.length > 0 ) { let idToken; idToken = jwt.decode(tokenSet.id_token); @@ -2192,6 +2663,40 @@ class BaseOauthPlugin extends BasePlugin { } } + if ( + pluginStrategy == PLUGIN_STRATEGY_OIDC && + _.get(plugin.config, "assertions.id_token_signature.enabled", false) + ) { + plugin.server.logger.debug("verifying id_token signature"); + async function getKey(header, callback) { + try { + let secret = _.get( + plugin.config, + "assertions.id_token_signature.key", + issuer.jwks_uri + ); + let key = await utils.get_jwt_sign_secret(secret, header.kid); + callback(null, key); + } catch (err) { + callback(err, null); + } + } + + try { + await new Promise((resolve, reject) => { + jwt.verify(tokenSet.id_token, getKey, {}, (err, decoded) => { + if (err) { + reject(err); + } + resolve(decoded); + }); + }); + } catch (err) { + plugin.server.logger.verbose(`id_token signature check failed: ${err}`); + return false; + } + } + return true; } @@ -2276,24 +2781,31 @@ class BaseOauthPlugin extends BasePlugin { return client; } - async get_authorization_url( - req, - parentReqInfo, - authorization_redirect_uri, - state - ) { + async get_authorization_url(req, parentReqInfo, params = {}) { const plugin = this; const client = await plugin.get_client(); - const url = client.authorizationUrl({ + //response_mode=fragment + const response_mode = plugin.config.response_mode; + const response_type = plugin.get_response_type(); + const scope = plugin.get_scope(); + + const url_input = { ...handlebars_template_properties( plugin.config.custom_authorization_parameters, { parentReqInfo, req } ), - redirect_uri: authorization_redirect_uri, - scope: plugin.config.scopes.join(" "), - state: state, - }); + ...params, + response_mode, + response_type, + scope, + }; + + const url = client.authorizationUrl(url_input); + + if (url_input.nonce) { + await plugin.save_nonce(url_input.nonce, { created: Date.now() }); + } return url; } @@ -2311,6 +2823,26 @@ class BaseOauthPlugin extends BasePlugin { }, }); } + + get_scopes() { + const plugin = this; + return _.get(plugin.config, "scopes", []); + } + + get_scope() { + const plugin = this; + return plugin.get_scopes().join(" "); + } + + get_response_types() { + const plugin = this; + return _.get(plugin.config, "response_types", ["code"]); + } + + get_response_type() { + const plugin = this; + return plugin.get_response_types().join(" "); + } } /** @@ -2329,17 +2861,24 @@ class OauthPlugin extends BaseOauthPlugin { super(...arguments); } - async authorization_code_callback(req, parentReqInfo, authorization_redirect_uri) { + async authorization_code_callback( + req, + parentReqInfo, + authorization_redirect_uri, + nonce, + code_verifier + ) { const plugin = this; const client = await plugin.get_client(); - const response_type = "code"; + const response_type = plugin.get_response_type(); return client.oauthCallback( authorization_redirect_uri, parentReqInfo.parsedQuery, { state: parentReqInfo.parsedQuery.state, - //nonce: null, + //nonce, OIDC only + code_verifier, response_type, }, { @@ -2540,17 +3079,24 @@ class OpenIdConnectPlugin extends BaseOauthPlugin { super(...arguments); } - async authorization_code_callback(req, parentReqInfo, authorization_redirect_uri) { + async authorization_code_callback( + req, + parentReqInfo, + authorization_redirect_uri, + nonce, + code_verifier + ) { const plugin = this; const client = await plugin.get_client(); - const response_type = "code"; + const response_type = plugin.get_response_type(); return client.callback( authorization_redirect_uri, parentReqInfo.parsedQuery, { state: parentReqInfo.parsedQuery.state, - //nonce: null, + nonce, + code_verifier, response_type, }, { diff --git a/src/server.js b/src/server.js index 2bb09a8..fd5ced2 100644 --- a/src/server.js +++ b/src/server.js @@ -14,6 +14,7 @@ const promBundle = require("express-prom-bundle"); const queryString = require("query-string"); const URI = require("uri-js"); +const YAML = require("yaml"); // auth plugins const { OauthPlugin, OpenIdConnectPlugin } = require("./plugin/oauth"); @@ -57,7 +58,7 @@ app.use( let revokedJtis = process.env["EAS_REVOKED_JTIS"]; if (revokedJtis) { - revokedJtis = JSON.parse(revokedJtis); + revokedJtis = YAML.parse(revokedJtis); } else { revokedJtis = []; } @@ -256,18 +257,38 @@ _verifyHandler = async (req, res, options = {}) => { configToken ); - // server-side tokens can be stored encrypted or not - if (!externalAuthServer.utils.is_jwt(configToken)) { - configToken = externalAuthServer.utils.decrypt( - externalAuthServer.secrets.config_token_encrypt_secret, - configToken - ); + // allow unsigned tokens + if (externalAuthServer.utils.is_json_like(configToken)) { + } else { } - configToken = jwt.verify( - configToken, - externalAuthServer.secrets.config_token_sign_secret - ); + let configTokenLoaded = false; + if (process.env.EAS_ALLOW_PLAIN_SERVER_SIDE_TOKENS) { + if ( + externalAuthServer.utils.is_json_like(configToken) || + externalAuthServer.utils.is_yaml_like(configToken) + ) { + try { + configToken = YAML.parse(configToken); + configTokenLoaded = true; + } catch (err) {} + } + } + + if (!configTokenLoaded) { + // server-side tokens can be stored encrypted or not + if (!externalAuthServer.utils.is_jwt(configToken)) { + configToken = externalAuthServer.utils.decrypt( + externalAuthServer.secrets.config_token_encrypt_secret, + configToken + ); + } + + configToken = jwt.verify( + configToken, + externalAuthServer.secrets.config_token_sign_secret + ); + } } configToken = externalAuthServer.setConfigTokenDefaults(configToken); diff --git a/src/store.js b/src/store.js index e6b6efa..d1dc45d 100644 --- a/src/store.js +++ b/src/store.js @@ -3,10 +3,11 @@ const { logger } = require("./logger"); const cacheManager = require("cache-manager"); const redisStore = require("cache-manager-redis-store"); const ioredisStore = require("cache-manager-ioredis"); +const YAML = require("yaml"); let cacheOpts = process.env["EAS_STORE_OPTS"]; if (cacheOpts) { - cacheOpts = JSON.parse(cacheOpts); + cacheOpts = YAML.parse(cacheOpts); } else { cacheOpts = { store: "memory", diff --git a/src/utils.js b/src/utils.js index 61757ad..94aab4f 100644 --- a/src/utils.js +++ b/src/utils.js @@ -3,7 +3,9 @@ const Handlebars = require("handlebars"); const jsonata = require("jsonata"); const jp = require("jsonpath"); const jq = require("node-jq"); +const jwksClient = require("jwks-rsa"); const queryString = require("query-string"); +const { retrieveSigningKeys } = require("jwks-rsa/src/utils"); const URI = require("uri-js"); const { v4: uuidv4 } = require("uuid"); @@ -128,6 +130,37 @@ function is_jwt(jwtString) { return re.test(jwtString); } +function is_json_like(str) { + if (typeof str !== "string") { + return false; + } + + const JSON_START = /^\[|^\{(?!\{)/; + const JSON_ENDS = { + "[": /]$/, + "{": /}$/, + }; + + const jsonStart = str.match(JSON_START); + if (!jsonStart) { + return false; + } + + return JSON_ENDS[jsonStart[0]].test(str); +} + +function is_yaml_like(str) { + if (typeof str !== "string") { + return false; + } + + if (str.startsWith("---")) { + return true; + } + // TODO: fix this logic to be sane + return true; +} + /** * ingress-nginx headers * - x-sent-from: nginx-ingress-controller @@ -405,6 +438,53 @@ function stringify(value) { function validateConfigToken(configToken) {} +async function get_jwt_sign_secret(secret, kid) { + // jwks uri + if ( + typeof secret === "string" && + (secret.startsWith("http://") || secret.startsWith("https://")) + ) { + const client = jwksClient({ + jwksUri: secret, + }); + + const key = await client.getSigningKey(kid); + if (key) { + return key.getPublicKey(); + } + } else { + // jwks result + if (typeof secret === "object" || Array.isArray(secret)) { + if (!Array.isArray(secret)) { + secret = secret.keys; + } + const keys = await retrieveSigningKeys(secret); + function getSigningKey() { + const kidDefined = kid !== undefined && kid !== null; + if (!kidDefined && keys.length > 1) { + throw new Error( + "No KID specified and JWKS endpoint returned more than 1 key" + ); + } + + const key = keys.find((k) => !kidDefined || k.kid === kid); + if (key) { + return key; + } else { + throw new Error(`Unable to find a signing key that matches '${kid}'`); + } + } + const key = getSigningKey(); + if (key) { + return key.getPublicKey(); + } + } + + // shared secret + return secret; + } +} + module.exports = { exit_failure, encrypt, @@ -414,6 +494,8 @@ module.exports = { base64_decode, generate_session_id, generate_csrf_id, + is_json_like, + is_yaml_like, is_jwt, get_parent_request_uri, get_parent_request_info, @@ -429,4 +511,5 @@ module.exports = { lower_case_keys, json_query, stringify, + get_jwt_sign_secret, }; From 61ffc3e56eaa616ffd04d7692d9eb744b9aa8603 Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Fri, 13 Jan 2023 17:27:53 -0700 Subject: [PATCH 2/8] remove no longer needed file_dev adapter Signed-off-by: Travis Glenn Hansen --- .../adapter/file_dev/index.js | 39 ------------------- 1 file changed, 39 deletions(-) delete mode 100644 src/config_token_store/adapter/file_dev/index.js diff --git a/src/config_token_store/adapter/file_dev/index.js b/src/config_token_store/adapter/file_dev/index.js deleted file mode 100644 index 51ad366..0000000 --- a/src/config_token_store/adapter/file_dev/index.js +++ /dev/null @@ -1,39 +0,0 @@ -const { FileConfigTokenStoreAdapter } = require("../file"); -const jwt = require("jsonwebtoken"); - -class FileDevConfigTokenStoreAdapter extends FileConfigTokenStoreAdapter { - /** - * Create new instance - * - * @name constructor - * @param {*} config - */ - constructor(server, config) { - super(...arguments); - } - - /** - * Retrieve to the token with the given id - * - * @param {*} id - */ - async getTokenInternal(id) { - const adapter = this; - let token = await FileConfigTokenStoreAdapter.prototype.getTokenInternal.call( - this, - ...arguments - ); - - token = jwt.sign(token, adapter.server.secrets.config_token_sign_secret); - token = adapter.server.utils.encrypt( - adapter.server.secrets.config_token_encrypt_secret, - token - ); - - return token; - } -} - -module.exports = { - FileDevConfigTokenStoreAdapter -}; From d3e744e146bd97510a59131743a0d75b16f262ed Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Fri, 13 Jan 2023 17:29:00 -0700 Subject: [PATCH 3/8] remove invalid reference Signed-off-by: Travis Glenn Hansen --- src/config_token_store/index.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/config_token_store/index.js b/src/config_token_store/index.js index f39b8e9..794c2dd 100644 --- a/src/config_token_store/index.js +++ b/src/config_token_store/index.js @@ -1,7 +1,6 @@ // config token store adapters const { EnvConfigTokenStoreAdapter } = require("./adapter/env"); const { FileConfigTokenStoreAdapter } = require("./adapter/file"); -const { FileDevConfigTokenStoreAdapter } = require("./adapter/file_dev"); const { SqlConfigTokenStoreAdapter } = require("./adapter/sql"); const YAML = require("yaml"); From 8258464fd0a23161a98714dc39ed454a3a85de5c Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Sat, 14 Jan 2023 10:20:27 -0700 Subject: [PATCH 4/8] support userinfo in client-side flows Signed-off-by: Travis Glenn Hansen --- src/plugin/oauth/index.js | 157 ++++++++++++++++++++++++-------------- 1 file changed, 100 insertions(+), 57 deletions(-) diff --git a/src/plugin/oauth/index.js b/src/plugin/oauth/index.js index bee794c..b4220a3 100644 --- a/src/plugin/oauth/index.js +++ b/src/plugin/oauth/index.js @@ -725,24 +725,24 @@ class BaseOauthPlugin extends BasePlugin { params = {}; Object.assign(params, paramsToObject(parsedHash.entries()), paramsToObject(parsedSearch.entries())); - log(parsedHash.entries()); - log(parsedSearch); - log(params); + log("hash params", parsedHash.entries()); + log("query params", parsedSearch); + log("normalized params", params); current_base_url = window.location.href.split(/[?#]/)[0]; - get_token_endpoint_url = current_base_url.replace('/oauth/callback-ua-client-code', '/oauth/client-token-url'); + get_client_endpoints_url = current_base_url.replace('/oauth/callback-ua-client-code', '/oauth/client-issuer-endpoints'); callback_url = current_base_url.replace('/oauth/callback-ua-client-code', '/oauth/callback'); callback_url += '?state=' + params.state; post_tokenset_endpoint_url = current_base_url.replace('/oauth/callback-ua-client-code', '/oauth/client-tokenset'); post_tokenset_endpoint_url += '?state=' + params.state; - log(get_token_endpoint_url); - log(callback_url); - log(post_tokenset_endpoint_url); + log({get_client_endpoints_url, callback_url, post_tokenset_endpoint_url}); let res; + let endpoints; let tokenSet; - res = await fetch(get_token_endpoint_url, { + let userinfo; + res = await fetch(get_client_endpoints_url, { //method: 'get' method: 'post', body: JSON.stringify(params), @@ -752,20 +752,33 @@ class BaseOauthPlugin extends BasePlugin { } }); - res = await res.json(); - log(res); + endpoints = await res.json(); + log("endpoints", endpoints); - res = await fetch(res.url, { + log("fetching tokenSet from op"); + res = await fetch(endpoints.decorated_token_endpoint, { method: 'post', }); - res = await res.json(); - log(res); - tokenSet = res; + tokenSet = await res.json(); + log("tokenSet", tokenSet); + + if (endpoints.features && endpoints.features.fetch_userinfo) { + log("fetching userinfo from op with tokenSet"); + res = await fetch(endpoints.userinfo_endpoint, { + method: 'get', + headers: { + 'Authorization': tokenSet.token_type + ' ' + tokenSet.access_token + } + }); + userinfo = await res.json(); + log("userinfo", userinfo); + } + log("sending data to eas"); res = await fetch(post_tokenset_endpoint_url, { //method: 'get' method: 'post', - body: JSON.stringify(tokenSet), + body: JSON.stringify({tokenSet, userinfo}), headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' @@ -785,56 +798,70 @@ class BaseOauthPlugin extends BasePlugin { /** * submit encrypted state and get the issuer token url in return + * only to be used internally by eas * */ - server.WebServer.post("/oauth/client-token-url", async (req, res) => { - server.logger.silly("%j", { - headers: req.headers, - body: req.body, - }); - - try { - let state = server.utils.decrypt( - issuer_encrypt_secret, - req.body.state, - "hex" - ); - state = jwt.verify(state, issuer_sign_secret); - state = await STORE_HELPER.get( - "state", - STATE_CACHE_PREFIX + state.state_id - ); + server.WebServer.post( + "/oauth/client-issuer-endpoints", + async (req, res) => { + server.logger.silly("%j", { + headers: req.headers, + body: req.body, + }); - const params = req.body; - params["grant_type"] = "authorization_code"; - params["client_id"] = state.client.client_id; - params["redirect_uri"] = state.authorization_redirect_uri; - if (state.nonce) { - params["nonce"] = state.nonce; - } + try { + let state = server.utils.decrypt( + issuer_encrypt_secret, + req.body.state, + "hex" + ); + state = jwt.verify(state, issuer_sign_secret); + state = await STORE_HELPER.get( + "state", + STATE_CACHE_PREFIX + state.state_id + ); - if (state.code_verifier) { - params["code_verifier"] = state.code_verifier; - } + const params = req.body; + params["grant_type"] = "authorization_code"; + params["client_id"] = state.client.client_id; + params["redirect_uri"] = state.authorization_redirect_uri; + if (state.nonce) { + params["nonce"] = state.nonce; + } - const queryString = Object.keys(params) - .map((key) => { - return ( - encodeURIComponent(key) + "=" + encodeURIComponent(params[key]) - ); - }) - .join("&"); + if (state.code_verifier) { + params["code_verifier"] = state.code_verifier; + } - res.json({ url: state.issuer.token_endpoint + "?" + queryString }); - } catch (e) { - res.statusCode = 401; - res.end(); + const queryString = Object.keys(params) + .map((key) => { + return ( + encodeURIComponent(key) + + "=" + + encodeURIComponent(params[key]) + ); + }) + .join("&"); + + res.json({ + decorated_token_endpoint: + state.issuer.token_endpoint + "?" + queryString, + token_endpoint: state.issuer.token_endpoint, + userinfo_endpoint: state.issuer.userinfo_endpoint, + jwks_uri: state.issuer.jwks_uri, + features: state.features, + }); + } catch (e) { + res.statusCode = 401; + res.end(); + } } - }); + ); /** * used to submit a tokenSet associated with a particlar nonce temporarily * until it can be validated and a session created + * only to be used internally by eas */ server.WebServer.post("/oauth/client-tokenset", async (req, res) => { server.logger.silly("%j", { @@ -868,7 +895,8 @@ class BaseOauthPlugin extends BasePlugin { return; } - nonceData.tokenSet = req.body; + nonceData.tokenSet = req.body.tokenSet; + nonceData.userinfo = req.body.userinfo; await STORE_HELPER.save( "nonce", NONCE_CACHE_PREFIX + state.nonce, @@ -1252,9 +1280,19 @@ class BaseOauthPlugin extends BasePlugin { request_is_xhr, nonce, nonce_ttl: _.get(plugin.config, "nonce.ttl", DEFAULT_NONCE_TTL), + features: { + fetch_userinfo: _.get( + plugin.config, + "features.fetch_userinfo", + false + ), + }, code_verifier, issuer: { - token_endpoint: issuer.token_endpoint, // add this in case of client-side token retrieval + // add this in case of client-side flows + token_endpoint: issuer.token_endpoint, + userinfo_endpoint: issuer.userinfo_endpoint, + jwks_uri: issuer.jwks_uri, }, client: { client_id: client.client_id, @@ -1680,7 +1718,12 @@ class BaseOauthPlugin extends BasePlugin { let userinfo; if (plugin.config.features.fetch_userinfo) { - userinfo = await plugin.get_userinfo(tokenSet); + // TODO: support clientProvidedUserinfo here + if (clientProvidedTokenSet) { + userinfo = { iat: Math.floor(Date.now() / 1000), data: nonceData.userinfo }; + } else { + userinfo = await plugin.get_userinfo(tokenSet); + } plugin.server.logger.verbose("userinfo %j", userinfo); if (userinfo && userinfo.data) { sessionPayload.userinfo = userinfo; From 43e6cdf7b2491d47286f64d83f2190021732b8cd Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Sat, 14 Jan 2023 20:32:27 -0700 Subject: [PATCH 5/8] better support for failure scenios with client-side flow Signed-off-by: Travis Glenn Hansen --- src/plugin/oauth/index.js | 43 +++++++++++++++++++++++++++++++++------ 1 file changed, 37 insertions(+), 6 deletions(-) diff --git a/src/plugin/oauth/index.js b/src/plugin/oauth/index.js index b4220a3..45c1e25 100644 --- a/src/plugin/oauth/index.js +++ b/src/plugin/oauth/index.js @@ -610,6 +610,7 @@ class BaseOauthPlugin extends BasePlugin { server.logger.silly("%j", { headers: req.headers, body: req.body, + query: req.query, }); try { @@ -624,6 +625,10 @@ class BaseOauthPlugin extends BasePlugin { STATE_CACHE_PREFIX + state.state_id ); + if (!state) { + throw new Error("failed load state"); + } + const state_redirect_uri = state.request_uri; const parsedStateRedirectURI = URI.parse(state_redirect_uri); @@ -664,6 +669,7 @@ class BaseOauthPlugin extends BasePlugin { server.logger.silly("%j", { headers: req.headers, body: req.body, + query: req.query, }); res.send(` @@ -694,6 +700,7 @@ class BaseOauthPlugin extends BasePlugin { server.logger.silly("%j", { headers: req.headers, body: req.body, + query: req.query, }); res.send(` @@ -731,13 +738,29 @@ class BaseOauthPlugin extends BasePlugin { current_base_url = window.location.href.split(/[?#]/)[0]; get_client_endpoints_url = current_base_url.replace('/oauth/callback-ua-client-code', '/oauth/client-issuer-endpoints'); - callback_url = current_base_url.replace('/oauth/callback-ua-client-code', '/oauth/callback'); - callback_url += '?state=' + params.state; post_tokenset_endpoint_url = current_base_url.replace('/oauth/callback-ua-client-code', '/oauth/client-tokenset'); post_tokenset_endpoint_url += '?state=' + params.state; + callback_url = current_base_url.replace('/oauth/callback-ua-client-code', '/oauth/callback'); + + for (const param of ["state", "error"]) { + if (params[param]) { + if (!callback_url.includes("?")) { + callback_url += "?"; + } else { + callback_url += "&"; + } + callback_url += param + "=" + encodeURIComponent(params[param]); + } + } + log({get_client_endpoints_url, callback_url, post_tokenset_endpoint_url}); + if (params.error) { + window.location.replace(callback_url); + return; + } + let res; let endpoints; let tokenSet; @@ -784,10 +807,11 @@ class BaseOauthPlugin extends BasePlugin { 'Content-Type': 'application/json' } }); - log(res); + res = await res.json(); + log("eas token response", res); window.location.replace(callback_url); - + })(); @@ -807,6 +831,7 @@ class BaseOauthPlugin extends BasePlugin { server.logger.silly("%j", { headers: req.headers, body: req.body, + query: req.query, }); try { @@ -867,6 +892,7 @@ class BaseOauthPlugin extends BasePlugin { server.logger.silly("%j", { headers: req.headers, body: req.body, + query: req.query, }); let state = server.utils.decrypt( @@ -905,13 +931,14 @@ class BaseOauthPlugin extends BasePlugin { ); res.statusCode = 200; - res.end(); + res.json({}); }); server.WebServer.get("/oauth/end-session-redirect", (req, res) => { server.logger.silly("%j", { headers: req.headers, body: req.body, + query: req.query, }); try { @@ -940,6 +967,7 @@ class BaseOauthPlugin extends BasePlugin { server.logger.silly("%j", { headers: req.headers, body: req.body, + query: req.query, }); try { @@ -1720,7 +1748,10 @@ class BaseOauthPlugin extends BasePlugin { if (plugin.config.features.fetch_userinfo) { // TODO: support clientProvidedUserinfo here if (clientProvidedTokenSet) { - userinfo = { iat: Math.floor(Date.now() / 1000), data: nonceData.userinfo }; + userinfo = { + iat: Math.floor(Date.now() / 1000), + data: nonceData.userinfo, + }; } else { userinfo = await plugin.get_userinfo(tokenSet); } From b26a1a2ec62e0163d3a3cc24cbfd5bf4bdd0151b Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Thu, 19 Jan 2023 07:53:12 -0700 Subject: [PATCH 6/8] support encoding injected headers, enforce presence of necessary forwarded headers Signed-off-by: Travis Glenn Hansen --- src/header/index.js | 24 ++++++++++++++++++++++-- src/utils.js | 12 ++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/header/index.js b/src/header/index.js index 610da8b..64ae127 100644 --- a/src/header/index.js +++ b/src/header/index.js @@ -2,11 +2,14 @@ const jp = require("jsonpath"); const jq = require("node-jq"); const jwt = require("jsonwebtoken"); const { logger } = require("../logger"); +const { base64_encode } = require("../utils"); class HeaderInjector { constructor(config, data) { this.config = config; this.data = data; + + console.log(data); } async injectHeaders(res) { @@ -22,6 +25,23 @@ class HeaderInjector { } else { value = this.data[headerConfig.source]; } + + if (typeof value !== "string") { + value = JSON.stringify(value); + } + + if (headerConfig.encoding) { + switch (headerConfig.encoding) { + case "base64": + value = base64_encode(value); + break; + default: + case "plain": + // noop + break; + } + } + this.setHeader(res, headerName, value); } catch (e) { logger.error("failed setting header: %s error: %s", headerName, e); @@ -50,7 +70,7 @@ class HeaderInjector { async jq_query(headerConfig, data) { const options = { input: "json", - output: "json" + output: "json", }; const values = await jq.run(headerConfig.query, data, options); @@ -121,5 +141,5 @@ class HeaderInjector { } module.exports = { - HeaderInjector + HeaderInjector, }; diff --git a/src/utils.js b/src/utils.js index 94aab4f..c047c5c 100644 --- a/src/utils.js +++ b/src/utils.js @@ -202,6 +202,18 @@ function get_parent_request_uri(req) { return req.headers["x-original-url"]; } + if (!("x-forwarded-proto" in req.headers)) { + throw new Error( + "missing x-forwarded-proto header, cannot determine parent request uri" + ); + } + + if (!("x-forwarded-uri" in req.headers)) { + throw new Error( + "missing x-forwarded-uri header, cannot determine parent request uri" + ); + } + originalRequestURI += req.headers["x-forwarded-proto"] + "://"; if (req.headers["x-forwarded-host"]) { From 6f91ef839ad04913d115107641fbc27624034c2a Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Thu, 19 Jan 2023 11:12:33 -0700 Subject: [PATCH 7/8] remove log entry Signed-off-by: Travis Glenn Hansen --- src/header/index.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/header/index.js b/src/header/index.js index 64ae127..ebfbc67 100644 --- a/src/header/index.js +++ b/src/header/index.js @@ -8,8 +8,6 @@ class HeaderInjector { constructor(config, data) { this.config = config; this.data = data; - - console.log(data); } async injectHeaders(res) { From 6f72df6171273bbba6d623227457ab6f79bb44ca Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Sun, 22 Jan 2023 21:10:57 -0700 Subject: [PATCH 8/8] doc new features, prep release Signed-off-by: Travis Glenn Hansen --- CHANGELOG.md | 17 + HEADERS.md | 1 + PLUGINS.md | 84 +++++ README.md | 4 + package-lock.json | 681 +++----------------------------------- src/plugin/oauth/index.js | 103 +++--- src/server.js | 6 +- 7 files changed, 215 insertions(+), 681 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c3ee400..37b9bf8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,20 @@ +# 0.13.0 + +Released 2023-01-22 + +- support additional `oauth` / `oidc` flows + - newly available callback endpoint `/oauth/callback-ua-client-code` which + retrieves the tokens via the browser instead of `eas` facilitating scenarios + where `eas` cannot directly communicate with `op` +- support `pkce` with `oauth` / `oidc` +- support `nonce` with `oidc` +- use server-side storage of `oauth` / `oidc` `state` data +- support `yaml` parsing in addition to `json` parsing in several locations +- introduce env var `EAS_ALLOW_PLAIN_SERVER_SIDE_TOKENS` to facilitate + server-side `config_tokens` being stored as simple json/yaml +- support `encoding` value of injected headers (plain (default), or base64) +- bump deps + # 0.12.5 Released 2023-01-04 diff --git a/HEADERS.md b/HEADERS.md index 764ccb4..c2ec435 100644 --- a/HEADERS.md +++ b/HEADERS.md @@ -41,6 +41,7 @@ as tokens etc may not exist. source: "userinfo",// userinfo, id_token, access_token, refresh_token, static, config_token, plugin_config, req, parentRequestInfo query_engine: "jp", query: "$.emails[*].email", // if left blank the data will be passed unaltered (ie: jwt encoded data) + encoding: "plain", // may be set to base64 query_engine: "jp", query: "$.login", diff --git a/PLUGINS.md b/PLUGINS.md index 16448f2..6847f48 100644 --- a/PLUGINS.md +++ b/PLUGINS.md @@ -326,16 +326,34 @@ Please read [further details](OAUTH_PLUGINS.md) about configuration. ``` { type: "oauth2", + issuer: { authorization_endpoint: 'https://accounts.google.com/o/oauth2/v2/auth', token_endpoint: 'https://www.googleapis.com/oauth2/v4/token', }, + client: { client_id: "...", client_secret: "..." }, + + // generally this should be unset and the provider default utilized (for authorization_code flow this is generally query) + // https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#ResponseTypesAndModes + //response_mode: , // query or fragment + + // generally this should be unset and the default utilized + // https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#ResponseTypesAndModes + //response_types: ["code"], + scopes: [], + // pkce settings + // https://oauth.net/2/pkce/ + pkce: { + enabled: false, + code_challenge_method: 'S256' // can also be 'plain' + }, + // custom authorization URL parameters // values can be handlebars syntax with access to `req` and `parentReqInfo` objects (see examples/parent_request_info.json) // NOTE: all critical fields are managed automatically, this should only be used in advanced scenarios @@ -550,8 +568,33 @@ Please read [further details](OAUTH_PLUGINS.md) about configuration. //registration_client_uri: "", //registration_access_token: "", }, + + // generally this should be unset and the provider default utilized (for authorization_code flow this is generally query) + // https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#ResponseTypesAndModes + //response_mode: , // query or fragment + + // generally this should be unset and the default utilized + // https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#ResponseTypesAndModes + //response_types: ["code"], + scopes: ["openid", "email", "profile"], // must include openid + // pkce settings + // https://oauth.net/2/pkce/ + pkce: { + enabled: false, + code_challenge_method: 'S256' // can also be 'plain' + }, + + // nonce settings + // https://openid.net/specs/openid-connect-core-1_0.html#NonceNotes + nonce: { + enabled: false, + // how long eas should retain nonce data + // note the nonce data is removed as quickly as possible during normal operation + ttl: 600 + }, + // custom authorization URL parameters // values can be handlebars syntax with access to `req` and `parentReqInfo` objects (see examples/parent_request_info.json) // NOTE: all critical fields are managed automatically, this should only be used in advanced scenarios @@ -579,6 +622,32 @@ Please read [further details](OAUTH_PLUGINS.md) about configuration. * if your oauth provider does not support wildcards place the URL configured in the provider (that will return to this proper service) here */ redirect_uri: "https://eas.example.com/oauth/callback", + + /** + * + * https://github.com/travisghansen/external-auth-server/issues/158 + * + * the /oauth/callback-ua-client-code endpoint will cause the browser/user-agent to directly exchange the code for token(s) instead of eas + * generally this highly unecessary, but can be used in scenarios where the brower/user-agent can communicate with both eas and op, + * but eas cannot directly access op + * + * in order to use this flow: + * - pkce enabled + * - nonce enabled + * - disable refresh_access_token + * - disable introspect_access_token + * - highly recommended to enable the sig(nature) assertion (otherwise session/token data can be spoofed) + * - if indeed eas cannot reach op, you will need to manually define the issuer endpoints instead of discovery url + * + * The general flow is: + * - eas directs the browser to op + * - after successful auth browser is redirected to /oauth/callback-ua-client-code + * - browser uses code in exchange for tokens and submits tokens to eas (using pure javascript) + * - browser is redirected to eas to continue remaining auth process + * + */ + //redirect_uri: "https://eas.example.com/oauth/callback-ua-client-code", + features: { /** * how to expire the cookie @@ -690,6 +759,11 @@ Please read [further details](OAUTH_PLUGINS.md) about configuration. }, }, assertions: { + /** + * assert the token(s) has the appropriate aud (client_id) + */ + aud: true, + /** * assert the token(s) has not expired */ @@ -705,6 +779,16 @@ Please read [further details](OAUTH_PLUGINS.md) about configuration. */ iss: true, + /** + * assert the token(s) has a valid signature + * usually only needed when using the /oauth/callback-ua-client-code redirect_uri with browser code exchange + */ + sig: { + enabled: false, + // defaults to issuer jwks endpoint, can be jwks response data or plain shared key/public key + // secret: + }, + /** * custom userinfo assertions */ diff --git a/README.md b/README.md index dea6458..3fb34f6 100644 --- a/README.md +++ b/README.md @@ -235,6 +235,10 @@ set the `config_token` `redirect_uri` to the `eas` service at the - `EAS_GRPC_PORT` port the grpc server is bound to (default is `50051`) - `EAS_GRPC_SSL_CERT` path to ssl cert file to enable https - `EAS_GRPC_SSL_KEY` path to ssl key file to enable https +- `EAS_ALLOW_EVAL` allow for potentially unsafe execution of untrusted code + (enables `request_js` and `js` query engine) +- `EAS_ALLOW_PLAIN_SERVER_SIDE_TOKENS` allows server-side token to be + unsigned (ie: store plain json/yaml in the store(s)) ## redis diff --git a/package-lock.json b/package-lock.json index dd4783a..11befef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -807,14 +807,6 @@ "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==" }, - "node_modules/@sindresorhus/is": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.7.0.tgz", - "integrity": "sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow==", - "engines": { - "node": ">=4" - } - }, "node_modules/@tediousjs/connection-string": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/@tediousjs/connection-string/-/connection-string-0.4.1.tgz", @@ -1129,25 +1121,6 @@ "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==" }, - "node_modules/archive-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/archive-type/-/archive-type-4.0.0.tgz", - "integrity": "sha512-zV4Ky0v1F8dBrdYElwTvQhweQ0P7Kwc1aluqJsYtOBP01jXcWCyW2IEfI1YiqsG+Iy7ZR+o5LF1N+PGECBxHWA==", - "dependencies": { - "file-type": "^4.2.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/archive-type/node_modules/file-type": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-4.4.0.tgz", - "integrity": "sha512-f2UbFQEk7LXgWpi5ntcO86OeA/cC80fuDDDaX/fZ2ZGel+AF7leRQqBBW1eJNiiQkrZlAoM6P+VYP5P6bOlDEQ==", - "engines": { - "node": ">=4" - } - }, "node_modules/are-we-there-yet": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", @@ -1835,36 +1808,6 @@ "node": ">= 16.18.0" } }, - "node_modules/cacheable-request": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-2.1.4.tgz", - "integrity": "sha512-vag0O2LKZ/najSoUwDbVlnlCFvhBE/7mGTY2B5FgCBDcRD+oVV1HYTOwM6JZfMg/hIcM6IwnTZ1uQQL5/X3xIQ==", - "dependencies": { - "clone-response": "1.0.2", - "get-stream": "3.0.0", - "http-cache-semantics": "3.8.1", - "keyv": "3.0.0", - "lowercase-keys": "1.0.0", - "normalize-url": "2.0.1", - "responselike": "1.0.2" - } - }, - "node_modules/cacheable-request/node_modules/get-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", - "integrity": "sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==", - "engines": { - "node": ">=4" - } - }, - "node_modules/cacheable-request/node_modules/lowercase-keys": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.0.tgz", - "integrity": "sha512-RPlX0+PHuvxVDZ7xX+EBVAp4RsVxP/TdDSN2mJYdiq1Lc4Hz7EUSjUI7RZrKKlmrIzVhf6Jo2stj7++gVarS0A==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/call-bind": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", @@ -1935,14 +1878,6 @@ "wrap-ansi": "^7.0.0" } }, - "node_modules/clone-response": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", - "integrity": "sha512-yjLXh88P599UOyPTFX0POsd7WxnbsVsGohcwzHOLspIhhpalPw1BcqED8NblyZLKcGrL8dTgMlcaZxV2jAD41Q==", - "dependencies": { - "mimic-response": "^1.0.0" - } - }, "node_modules/cluster-key-slot": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", @@ -2332,66 +2267,6 @@ "node": ">=8" } }, - "node_modules/download": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/download/-/download-8.0.0.tgz", - "integrity": "sha512-ASRY5QhDk7FK+XrQtQyvhpDKanLluEEQtWl/J7Lxuf/b+i8RYh997QeXvL85xitrmRKVlx9c7eTrcRdq2GS4eA==", - "dependencies": { - "archive-type": "^4.0.0", - "content-disposition": "^0.5.2", - "decompress": "^4.2.1", - "ext-name": "^5.0.0", - "file-type": "^11.1.0", - "filenamify": "^3.0.0", - "get-stream": "^4.1.0", - "got": "^8.3.1", - "make-dir": "^2.1.0", - "p-event": "^2.1.0", - "pify": "^4.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/download/node_modules/file-type": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-11.1.0.tgz", - "integrity": "sha512-rM0UO7Qm9K7TWTtA6AShI/t7H5BPjDeGVDaNyg9BjHAj3PysKy7+8C8D137R88jnR3rFJZQB/tFgydl5sN5m7g==", - "engines": { - "node": ">=6" - } - }, - "node_modules/download/node_modules/get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/download/node_modules/make-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", - "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", - "dependencies": { - "pify": "^4.0.1", - "semver": "^5.6.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/download/node_modules/pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", - "engines": { - "node": ">=6" - } - }, "node_modules/dtrace-provider": { "version": "0.8.8", "resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.8.8.tgz", @@ -2954,19 +2829,6 @@ "node": ">=4" } }, - "node_modules/filenamify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-3.0.0.tgz", - "integrity": "sha512-5EFZ//MsvJgXjBAFJ+Bh2YaCTRF/VP1YOmGrgt+KJ4SFRLjI87EIdwLLuT6wQX0I4F9W41xutobzczjsOKlI/g==", - "dependencies": { - "filename-reserved-regex": "^2.0.0", - "strip-outer": "^1.0.0", - "trim-repeated": "^1.0.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/filter-obj": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz", @@ -3042,15 +2904,6 @@ "node": ">= 0.6" } }, - "node_modules/from2": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", - "integrity": "sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==", - "dependencies": { - "inherits": "^2.0.1", - "readable-stream": "^2.0.0" - } - }, "node_modules/fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", @@ -3289,49 +3142,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/got": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/got/-/got-8.3.2.tgz", - "integrity": "sha512-qjUJ5U/hawxosMryILofZCkm3C84PLJS/0grRIpjAwu+Lkxxj5cxeCU25BG0/3mDSpXKTyZr8oh8wIgLaH0QCw==", - "dependencies": { - "@sindresorhus/is": "^0.7.0", - "cacheable-request": "^2.1.1", - "decompress-response": "^3.3.0", - "duplexer3": "^0.1.4", - "get-stream": "^3.0.0", - "into-stream": "^3.1.0", - "is-retry-allowed": "^1.1.0", - "isurl": "^1.0.0-alpha5", - "lowercase-keys": "^1.0.0", - "mimic-response": "^1.0.0", - "p-cancelable": "^0.4.0", - "p-timeout": "^2.0.1", - "pify": "^3.0.0", - "safe-buffer": "^5.1.1", - "timed-out": "^4.0.1", - "url-parse-lax": "^3.0.0", - "url-to-options": "^1.0.1" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/got/node_modules/get-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", - "integrity": "sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==", - "engines": { - "node": ">=4" - } - }, - "node_modules/got/node_modules/pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", - "engines": { - "node": ">=4" - } - }, "node_modules/graceful-fs": { "version": "4.2.10", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", @@ -3480,11 +3290,6 @@ "xerror": "^1.1.2" } }, - "node_modules/http-cache-semantics": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz", - "integrity": "sha512-5ai2iksyV8ZXmnZhHH4rWPoxxistEexSi5936zIQ1bnNTW5VnA85B6P/VpXiRM017IgRvb2kKo1a//y+0wSp3w==" - }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -3687,18 +3492,6 @@ "node": ">= 0.10" } }, - "node_modules/into-stream": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/into-stream/-/into-stream-3.1.0.tgz", - "integrity": "sha512-TcdjPibTksa1NQximqep2r17ISRiNE9fwlfbg3F8ANdvP5/yrFTew86VcO//jk4QTaMlbjypPBq76HN2zaKfZQ==", - "dependencies": { - "from2": "^2.1.1", - "p-is-promise": "^1.1.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/ioredis": { "version": "4.28.5", "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-4.28.5.tgz", @@ -4156,11 +3949,6 @@ "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==" }, - "node_modules/json-buffer": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", - "integrity": "sha512-CuUqjv0FUZIdXkHPI8MezCnFCdaTAacej1TZYulLoAg1h/PhwkdXFN4V/gzY4g+fMBCOV2xF+rp7t2XD2ns/NQ==" - }, "node_modules/json-schema": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", @@ -4360,18 +4148,10 @@ "safe-buffer": "^5.0.1" } }, - "node_modules/keyv": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.0.0.tgz", - "integrity": "sha512-eguHnq22OE3uVoSYG0LVWNP+4ppamWr9+zWBe1bsNcovIMy6huUJFPgy4mGwCd/rnl3vOLGW1MTlu4c57CT1xA==", - "dependencies": { - "json-buffer": "3.0.0" - } - }, "node_modules/knex": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/knex/-/knex-2.4.0.tgz", - "integrity": "sha512-i0GWwqYp1Hs2yvc2rlDO6nzzkLhwdyOZKRdsMTB8ZxOs2IXQyL5rBjSbS1krowCh6V65T4X9CJaKtuIfkaPGSA==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/knex/-/knex-2.4.2.tgz", + "integrity": "sha512-tMI1M7a+xwHhPxjbl/H9K1kHX+VncEYcvCx5K00M16bWvpYPKAZd6QrCu68PtHAdIZNQPWZn0GVhqVBEthGWCg==", "dependencies": { "colorette": "2.0.19", "commander": "^9.1.0", @@ -4970,9 +4750,9 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, "node_modules/mssql": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/mssql/-/mssql-9.0.1.tgz", - "integrity": "sha512-rmBEPeUgZswvcclSc4s21WJCG0xqDYBas1MSnZepzDPvtNRgkx6f/gCm52EEo2djVtIXJgRq22m7gj7floz4Ng==", + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/mssql/-/mssql-9.1.1.tgz", + "integrity": "sha512-m0yTx9xzUtTvJpWJHqknUXUDPRnJXZYOOFNygnNIXn1PBkLsC/rkXQdquObd+M0ZPlBhGC00Jg28zG0wCl7VWg==", "dependencies": { "@tediousjs/connection-string": "^0.4.1", "commander": "^9.4.0", @@ -5174,6 +4954,17 @@ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz", "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==" }, + "node_modules/node-downloader-helper": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/node-downloader-helper/-/node-downloader-helper-2.1.6.tgz", + "integrity": "sha512-VkOvAXIopI3xMuM/MC5UL7NqqnizQ/9QXZt28jR8FPZ6fHLQm4xe4+YXJ9FqsWwLho5BLXrF51nfOQ0QcohRkQ==", + "bin": { + "ndh": "bin/ndh" + }, + "engines": { + "node": ">=14.18" + } + }, "node_modules/node-fetch": { "version": "2.6.7", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", @@ -5356,15 +5147,15 @@ } }, "node_modules/node-jq": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/node-jq/-/node-jq-2.3.4.tgz", - "integrity": "sha512-Zm0kJzDHUtVdS9a3Ypq07+rQfxJ9SdbUa5hEOP9Y+gwToLPme6onIptApQgsqIMByA1FIE7sw+EKZvrmIy1Jqw==", + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/node-jq/-/node-jq-2.3.5.tgz", + "integrity": "sha512-V2qmX4oXvrITgxeR8WZTELVGFGx2auO+qvxs7Q4Isovh2UGkO1Fl3OACrxPZ2dgikJT5KbwYvK8PwGyc2hfRIg==", "hasInstallScript": true, "dependencies": { "bin-build": "^3.0.0", - "download": "^8.0.0", "is-valid-path": "^0.1.1", "joi": "^17.4.0", + "node-downloader-helper": "^2.1.6", "strip-final-newline": "^2.0.0", "tempfile": "^3.0.0" }, @@ -5386,40 +5177,6 @@ "node": ">=6" } }, - "node_modules/normalize-url": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-2.0.1.tgz", - "integrity": "sha512-D6MUW4K/VzoJ4rJ01JFKxDrtY1v9wrgzCX5f2qj/lzH1m/lW6MhUZFKerVsnyjOhOsYzI9Kqqak+10l4LvLpMw==", - "dependencies": { - "prepend-http": "^2.0.0", - "query-string": "^5.0.1", - "sort-keys": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/normalize-url/node_modules/query-string": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-5.1.1.tgz", - "integrity": "sha512-gjWOsm2SoGlgLEdAGt7a6slVOk9mGiXmPFMqrEhLQ68rhQuBnpfs3+EmlvqKyxnCo9/PPlF+9MtY02S1aFg+Jw==", - "dependencies": { - "decode-uri-component": "^0.2.0", - "object-assign": "^4.1.0", - "strict-uri-encode": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/normalize-url/node_modules/strict-uri-encode": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", - "integrity": "sha512-R3f198pcvnB+5IpnBlRkphuE9n46WyVl8I39W/ZUTZLz4nqSP/oLYUrcnJrw462Ds8he4YKMov2efsTIw1BDGQ==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/npm-conf": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/npm-conf/-/npm-conf-1.1.3.tgz", @@ -5571,9 +5328,9 @@ } }, "node_modules/openid-client": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.3.1.tgz", - "integrity": "sha512-RLfehQiHch9N6tRWNx68cicf3b1WR0x74bJWHRc25uYIbSRwjxYcTFaRnzbbpls5jroLAaB/bFIodTgA5LJMvw==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.3.2.tgz", + "integrity": "sha512-nXXt+cna0XHOw+WqjMZOmuXw/YZEMwfWD2lD7tCsFtsBjMQGVXA+NZABA3upYBET1suhIsmfd7GnxG4jCAnvYQ==", "dependencies": { "jose": "^4.10.0", "lru-cache": "^6.0.0", @@ -5631,25 +5388,6 @@ "node": ">=0.6.0" } }, - "node_modules/p-cancelable": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-0.4.1.tgz", - "integrity": "sha512-HNa1A8LvB1kie7cERyy21VNeHb2CWJJYqyyC2o3klWFfMGlFmWv2Z7sFgZH8ZiaYL95ydToKTFVXgMV/Os0bBQ==", - "engines": { - "node": ">=4" - } - }, - "node_modules/p-event": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/p-event/-/p-event-2.3.1.tgz", - "integrity": "sha512-NQCqOFhbpVTMX4qMe8PF8lbGtzZ+LCiN7pcNrb/413Na7+TRoe1xkKUzuWa/YEJdGQ0FvKtj35EEbDoVPO2kbA==", - "dependencies": { - "p-timeout": "^2.0.1" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/p-finally": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", @@ -5658,14 +5396,6 @@ "node": ">=4" } }, - "node_modules/p-is-promise": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-1.1.0.tgz", - "integrity": "sha512-zL7VE4JVS2IFSkR2GQKDSPEVxkoH43/p7oEnwpdCndKYJO0HVeRB7fA8TJwuLOTBREtK0ea8eHaxdwcpob5dmg==", - "engines": { - "node": ">=4" - } - }, "node_modules/p-map": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", @@ -5693,17 +5423,6 @@ "node": ">=4" } }, - "node_modules/p-timeout": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-2.0.1.tgz", - "integrity": "sha512-88em58dDVB/KzPEx1X0N3LwFfYZPyDc4B6eF38M1rk9VTZMbxXXgjugz8mmwpS9Ox4BDZ+t6t3QP5+/gazweIA==", - "dependencies": { - "p-finally": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/packet-reader": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz", @@ -5905,14 +5624,6 @@ "node": ">= 0.8.0" } }, - "node_modules/prepend-http": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", - "integrity": "sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA==", - "engines": { - "node": ">=4" - } - }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -6003,15 +5714,6 @@ "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==" }, - "node_modules/pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, "node_modules/punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", @@ -6264,14 +5966,6 @@ "node": ">=8" } }, - "node_modules/responselike": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", - "integrity": "sha512-/Fpe5guzJk1gPqdJLJR5u7eG/gNY4nImjbRDaVWVMRhne55TCmj2i9Q+54PBRfatRC8v/rIiv9BN0pMd9OV5EQ==", - "dependencies": { - "lowercase-keys": "^1.0.0" - } - }, "node_modules/retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", @@ -6588,17 +6282,6 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "optional": true }, - "node_modules/sort-keys": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-2.0.0.tgz", - "integrity": "sha512-/dPCrG1s3ePpWm6yBbxZq5Be1dXGLyLn9Z791chDC3NFrpkVbWGzkBwPN1knaciexFXgRJ7hzdnwZ4stHSDmjg==", - "dependencies": { - "is-plain-obj": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/sort-keys-length": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/sort-keys-length/-/sort-keys-length-1.0.1.tgz", @@ -7332,17 +7015,6 @@ "punycode": "^2.1.0" } }, - "node_modules/url-parse-lax": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", - "integrity": "sha512-NjFKA0DidqPa5ciFcSrXnAltTtzz84ogy+NebPvfEgAck0+TNg4UJ4IN+fB7zRZfbgUf0syOo9MDxFkDSMuFaQ==", - "dependencies": { - "prepend-http": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/url-to-options": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/url-to-options/-/url-to-options-1.0.1.tgz", @@ -8282,11 +7954,6 @@ "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==" }, - "@sindresorhus/is": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.7.0.tgz", - "integrity": "sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow==" - }, "@tediousjs/connection-string": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/@tediousjs/connection-string/-/connection-string-0.4.1.tgz", @@ -8548,21 +8215,6 @@ "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==" }, - "archive-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/archive-type/-/archive-type-4.0.0.tgz", - "integrity": "sha512-zV4Ky0v1F8dBrdYElwTvQhweQ0P7Kwc1aluqJsYtOBP01jXcWCyW2IEfI1YiqsG+Iy7ZR+o5LF1N+PGECBxHWA==", - "requires": { - "file-type": "^4.2.0" - }, - "dependencies": { - "file-type": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-4.4.0.tgz", - "integrity": "sha512-f2UbFQEk7LXgWpi5ntcO86OeA/cC80fuDDDaX/fZ2ZGel+AF7leRQqBBW1eJNiiQkrZlAoM6P+VYP5P6bOlDEQ==" - } - } - }, "are-we-there-yet": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", @@ -9103,32 +8755,6 @@ "redis": "^4.3.1" } }, - "cacheable-request": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-2.1.4.tgz", - "integrity": "sha512-vag0O2LKZ/najSoUwDbVlnlCFvhBE/7mGTY2B5FgCBDcRD+oVV1HYTOwM6JZfMg/hIcM6IwnTZ1uQQL5/X3xIQ==", - "requires": { - "clone-response": "1.0.2", - "get-stream": "3.0.0", - "http-cache-semantics": "3.8.1", - "keyv": "3.0.0", - "lowercase-keys": "1.0.0", - "normalize-url": "2.0.1", - "responselike": "1.0.2" - }, - "dependencies": { - "get-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", - "integrity": "sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==" - }, - "lowercase-keys": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.0.tgz", - "integrity": "sha512-RPlX0+PHuvxVDZ7xX+EBVAp4RsVxP/TdDSN2mJYdiq1Lc4Hz7EUSjUI7RZrKKlmrIzVhf6Jo2stj7++gVarS0A==" - } - } - }, "call-bind": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", @@ -9184,14 +8810,6 @@ "wrap-ansi": "^7.0.0" } }, - "clone-response": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", - "integrity": "sha512-yjLXh88P599UOyPTFX0POsd7WxnbsVsGohcwzHOLspIhhpalPw1BcqED8NblyZLKcGrL8dTgMlcaZxV2jAD41Q==", - "requires": { - "mimic-response": "^1.0.0" - } - }, "cluster-key-slot": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", @@ -9504,53 +9122,6 @@ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz", "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==" }, - "download": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/download/-/download-8.0.0.tgz", - "integrity": "sha512-ASRY5QhDk7FK+XrQtQyvhpDKanLluEEQtWl/J7Lxuf/b+i8RYh997QeXvL85xitrmRKVlx9c7eTrcRdq2GS4eA==", - "requires": { - "archive-type": "^4.0.0", - "content-disposition": "^0.5.2", - "decompress": "^4.2.1", - "ext-name": "^5.0.0", - "file-type": "^11.1.0", - "filenamify": "^3.0.0", - "get-stream": "^4.1.0", - "got": "^8.3.1", - "make-dir": "^2.1.0", - "p-event": "^2.1.0", - "pify": "^4.0.1" - }, - "dependencies": { - "file-type": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-11.1.0.tgz", - "integrity": "sha512-rM0UO7Qm9K7TWTtA6AShI/t7H5BPjDeGVDaNyg9BjHAj3PysKy7+8C8D137R88jnR3rFJZQB/tFgydl5sN5m7g==" - }, - "get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "requires": { - "pump": "^3.0.0" - } - }, - "make-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", - "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", - "requires": { - "pify": "^4.0.1", - "semver": "^5.6.0" - } - }, - "pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==" - } - } - }, "dtrace-provider": { "version": "0.8.8", "resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.8.8.tgz", @@ -9975,16 +9546,6 @@ "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz", "integrity": "sha512-lc1bnsSr4L4Bdif8Xb/qrtokGbq5zlsms/CYH8PP+WtCkGNF65DPiQY8vG3SakEdRn8Dlnm+gW/qWKKjS5sZzQ==" }, - "filenamify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-3.0.0.tgz", - "integrity": "sha512-5EFZ//MsvJgXjBAFJ+Bh2YaCTRF/VP1YOmGrgt+KJ4SFRLjI87EIdwLLuT6wQX0I4F9W41xutobzczjsOKlI/g==", - "requires": { - "filename-reserved-regex": "^2.0.0", - "strip-outer": "^1.0.0", - "trim-repeated": "^1.0.0" - } - }, "filter-obj": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz", @@ -10042,15 +9603,6 @@ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" }, - "from2": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", - "integrity": "sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==", - "requires": { - "inherits": "^2.0.1", - "readable-stream": "^2.0.0" - } - }, "fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", @@ -10234,42 +9786,6 @@ "get-intrinsic": "^1.1.3" } }, - "got": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/got/-/got-8.3.2.tgz", - "integrity": "sha512-qjUJ5U/hawxosMryILofZCkm3C84PLJS/0grRIpjAwu+Lkxxj5cxeCU25BG0/3mDSpXKTyZr8oh8wIgLaH0QCw==", - "requires": { - "@sindresorhus/is": "^0.7.0", - "cacheable-request": "^2.1.1", - "decompress-response": "^3.3.0", - "duplexer3": "^0.1.4", - "get-stream": "^3.0.0", - "into-stream": "^3.1.0", - "is-retry-allowed": "^1.1.0", - "isurl": "^1.0.0-alpha5", - "lowercase-keys": "^1.0.0", - "mimic-response": "^1.0.0", - "p-cancelable": "^0.4.0", - "p-timeout": "^2.0.1", - "pify": "^3.0.0", - "safe-buffer": "^5.1.1", - "timed-out": "^4.0.1", - "url-parse-lax": "^3.0.0", - "url-to-options": "^1.0.1" - }, - "dependencies": { - "get-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", - "integrity": "sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==" - }, - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==" - } - } - }, "graceful-fs": { "version": "4.2.10", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", @@ -10370,11 +9886,6 @@ "xerror": "^1.1.2" } }, - "http-cache-semantics": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz", - "integrity": "sha512-5ai2iksyV8ZXmnZhHH4rWPoxxistEexSi5936zIQ1bnNTW5VnA85B6P/VpXiRM017IgRvb2kKo1a//y+0wSp3w==" - }, "http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -10523,15 +10034,6 @@ "resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz", "integrity": "sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==" }, - "into-stream": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/into-stream/-/into-stream-3.1.0.tgz", - "integrity": "sha512-TcdjPibTksa1NQximqep2r17ISRiNE9fwlfbg3F8ANdvP5/yrFTew86VcO//jk4QTaMlbjypPBq76HN2zaKfZQ==", - "requires": { - "from2": "^2.1.1", - "p-is-promise": "^1.1.0" - } - }, "ioredis": { "version": "4.28.5", "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-4.28.5.tgz", @@ -10852,11 +10354,6 @@ "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==" }, - "json-buffer": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", - "integrity": "sha512-CuUqjv0FUZIdXkHPI8MezCnFCdaTAacej1TZYulLoAg1h/PhwkdXFN4V/gzY4g+fMBCOV2xF+rp7t2XD2ns/NQ==" - }, "json-schema": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", @@ -11019,18 +10516,10 @@ "safe-buffer": "^5.0.1" } }, - "keyv": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.0.0.tgz", - "integrity": "sha512-eguHnq22OE3uVoSYG0LVWNP+4ppamWr9+zWBe1bsNcovIMy6huUJFPgy4mGwCd/rnl3vOLGW1MTlu4c57CT1xA==", - "requires": { - "json-buffer": "3.0.0" - } - }, "knex": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/knex/-/knex-2.4.0.tgz", - "integrity": "sha512-i0GWwqYp1Hs2yvc2rlDO6nzzkLhwdyOZKRdsMTB8ZxOs2IXQyL5rBjSbS1krowCh6V65T4X9CJaKtuIfkaPGSA==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/knex/-/knex-2.4.2.tgz", + "integrity": "sha512-tMI1M7a+xwHhPxjbl/H9K1kHX+VncEYcvCx5K00M16bWvpYPKAZd6QrCu68PtHAdIZNQPWZn0GVhqVBEthGWCg==", "requires": { "colorette": "2.0.19", "commander": "^9.1.0", @@ -11481,9 +10970,9 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, "mssql": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/mssql/-/mssql-9.0.1.tgz", - "integrity": "sha512-rmBEPeUgZswvcclSc4s21WJCG0xqDYBas1MSnZepzDPvtNRgkx6f/gCm52EEo2djVtIXJgRq22m7gj7floz4Ng==", + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/mssql/-/mssql-9.1.1.tgz", + "integrity": "sha512-m0yTx9xzUtTvJpWJHqknUXUDPRnJXZYOOFNygnNIXn1PBkLsC/rkXQdquObd+M0ZPlBhGC00Jg28zG0wCl7VWg==", "requires": { "@tediousjs/connection-string": "^0.4.1", "commander": "^9.4.0", @@ -11646,6 +11135,11 @@ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz", "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==" }, + "node-downloader-helper": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/node-downloader-helper/-/node-downloader-helper-2.1.6.tgz", + "integrity": "sha512-VkOvAXIopI3xMuM/MC5UL7NqqnizQ/9QXZt28jR8FPZ6fHLQm4xe4+YXJ9FqsWwLho5BLXrF51nfOQ0QcohRkQ==" + }, "node-fetch": { "version": "2.6.7", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", @@ -11774,14 +11268,14 @@ } }, "node-jq": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/node-jq/-/node-jq-2.3.4.tgz", - "integrity": "sha512-Zm0kJzDHUtVdS9a3Ypq07+rQfxJ9SdbUa5hEOP9Y+gwToLPme6onIptApQgsqIMByA1FIE7sw+EKZvrmIy1Jqw==", + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/node-jq/-/node-jq-2.3.5.tgz", + "integrity": "sha512-V2qmX4oXvrITgxeR8WZTELVGFGx2auO+qvxs7Q4Isovh2UGkO1Fl3OACrxPZ2dgikJT5KbwYvK8PwGyc2hfRIg==", "requires": { "bin-build": "^3.0.0", - "download": "^8.0.0", "is-valid-path": "^0.1.1", "joi": "^17.4.0", + "node-downloader-helper": "^2.1.6", "strip-final-newline": "^2.0.0", "tempfile": "^3.0.0" } @@ -11794,33 +11288,6 @@ "abbrev": "1" } }, - "normalize-url": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-2.0.1.tgz", - "integrity": "sha512-D6MUW4K/VzoJ4rJ01JFKxDrtY1v9wrgzCX5f2qj/lzH1m/lW6MhUZFKerVsnyjOhOsYzI9Kqqak+10l4LvLpMw==", - "requires": { - "prepend-http": "^2.0.0", - "query-string": "^5.0.1", - "sort-keys": "^2.0.0" - }, - "dependencies": { - "query-string": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-5.1.1.tgz", - "integrity": "sha512-gjWOsm2SoGlgLEdAGt7a6slVOk9mGiXmPFMqrEhLQ68rhQuBnpfs3+EmlvqKyxnCo9/PPlF+9MtY02S1aFg+Jw==", - "requires": { - "decode-uri-component": "^0.2.0", - "object-assign": "^4.1.0", - "strict-uri-encode": "^1.0.0" - } - }, - "strict-uri-encode": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", - "integrity": "sha512-R3f198pcvnB+5IpnBlRkphuE9n46WyVl8I39W/ZUTZLz4nqSP/oLYUrcnJrw462Ds8he4YKMov2efsTIw1BDGQ==" - } - } - }, "npm-conf": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/npm-conf/-/npm-conf-1.1.3.tgz", @@ -11932,9 +11399,9 @@ } }, "openid-client": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.3.1.tgz", - "integrity": "sha512-RLfehQiHch9N6tRWNx68cicf3b1WR0x74bJWHRc25uYIbSRwjxYcTFaRnzbbpls5jroLAaB/bFIodTgA5LJMvw==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.3.2.tgz", + "integrity": "sha512-nXXt+cna0XHOw+WqjMZOmuXw/YZEMwfWD2lD7tCsFtsBjMQGVXA+NZABA3upYBET1suhIsmfd7GnxG4jCAnvYQ==", "requires": { "jose": "^4.10.0", "lru-cache": "^6.0.0", @@ -11978,29 +11445,11 @@ "resolved": "https://registry.npmjs.org/oracle/-/oracle-0.4.1.tgz", "integrity": "sha512-T3F+ERMMVIDyHyfF58QFQKSX/tEXayierSLp5lE9h2FjB20iVRviKb+Ze4roknlwLBF6kyaXBlDdOkA4COc34w==" }, - "p-cancelable": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-0.4.1.tgz", - "integrity": "sha512-HNa1A8LvB1kie7cERyy21VNeHb2CWJJYqyyC2o3klWFfMGlFmWv2Z7sFgZH8ZiaYL95ydToKTFVXgMV/Os0bBQ==" - }, - "p-event": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/p-event/-/p-event-2.3.1.tgz", - "integrity": "sha512-NQCqOFhbpVTMX4qMe8PF8lbGtzZ+LCiN7pcNrb/413Na7+TRoe1xkKUzuWa/YEJdGQ0FvKtj35EEbDoVPO2kbA==", - "requires": { - "p-timeout": "^2.0.1" - } - }, "p-finally": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==" }, - "p-is-promise": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-1.1.0.tgz", - "integrity": "sha512-zL7VE4JVS2IFSkR2GQKDSPEVxkoH43/p7oEnwpdCndKYJO0HVeRB7fA8TJwuLOTBREtK0ea8eHaxdwcpob5dmg==" - }, "p-map": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", @@ -12019,14 +11468,6 @@ "resolved": "https://registry.npmjs.org/p-reduce/-/p-reduce-1.0.0.tgz", "integrity": "sha512-3Tx1T3oM1xO/Y8Gj0sWyE78EIJZ+t+aEmXUdvQgvGmSMri7aPTHoovbXEreWKkL5j21Er60XAWLTzKbAKYOujQ==" }, - "p-timeout": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-2.0.1.tgz", - "integrity": "sha512-88em58dDVB/KzPEx1X0N3LwFfYZPyDc4B6eF38M1rk9VTZMbxXXgjugz8mmwpS9Ox4BDZ+t6t3QP5+/gazweIA==", - "requires": { - "p-finally": "^1.0.0" - } - }, "packet-reader": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz", @@ -12173,11 +11614,6 @@ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==" }, - "prepend-http": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", - "integrity": "sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA==" - }, "process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -12257,15 +11693,6 @@ "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==" }, - "pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, "punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", @@ -12455,14 +11882,6 @@ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==" }, - "responselike": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", - "integrity": "sha512-/Fpe5guzJk1gPqdJLJR5u7eG/gNY4nImjbRDaVWVMRhne55TCmj2i9Q+54PBRfatRC8v/rIiv9BN0pMd9OV5EQ==", - "requires": { - "lowercase-keys": "^1.0.0" - } - }, "retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", @@ -12704,14 +12123,6 @@ } } }, - "sort-keys": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-2.0.0.tgz", - "integrity": "sha512-/dPCrG1s3ePpWm6yBbxZq5Be1dXGLyLn9Z791chDC3NFrpkVbWGzkBwPN1knaciexFXgRJ7hzdnwZ4stHSDmjg==", - "requires": { - "is-plain-obj": "^1.0.0" - } - }, "sort-keys-length": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/sort-keys-length/-/sort-keys-length-1.0.1.tgz", @@ -13265,14 +12676,6 @@ "punycode": "^2.1.0" } }, - "url-parse-lax": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", - "integrity": "sha512-NjFKA0DidqPa5ciFcSrXnAltTtzz84ogy+NebPvfEgAck0+TNg4UJ4IN+fB7zRZfbgUf0syOo9MDxFkDSMuFaQ==", - "requires": { - "prepend-http": "^2.0.0" - } - }, "url-to-options": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/url-to-options/-/url-to-options-1.0.1.tgz", diff --git a/src/plugin/oauth/index.js b/src/plugin/oauth/index.js index 45c1e25..00aafae 100644 --- a/src/plugin/oauth/index.js +++ b/src/plugin/oauth/index.js @@ -217,16 +217,33 @@ function initialize_common_config_options(config) { } } -function token_is_expired(token) { - return !!(token.exp && token.exp < Date.now() / 1000); -} +function tokenset_signature_is_trusted(tokenSet, secret) { + if (tokenSet.id_token) { + return token_signature_is_trusted(tokenSet.id_token, secret); + } -function token_is_premature(token) { - return !!(token.nbf && token.nbf < Date.now() / 1000); + return false; } -function token_issuer_match(token, issuer) { - return token.iss && token.iss == issuer; +async function token_signature_is_trusted(token, secret) { + async function getKey(header, callback) { + try { + let key = await utils.get_jwt_sign_secret(secret, header.kid); + callback(null, key); + } catch (err) { + callback(err, null); + } + } + + return new Promise((resolve, reject) => { + jwt.verify(token, getKey, {}, (err, decoded) => { + if (err) { + reject(err); + return; + } + resolve(decoded); + }); + }); } function tokenset_is_expired(tokenSet) { @@ -241,6 +258,10 @@ function tokenset_is_expired(tokenSet) { return false; } +function token_is_expired(token) { + return !!(token.exp && token.exp < Date.now() / 1000); +} + function tokenset_is_premature(tokenSet) { if (tokenSet.id_token) { return token_is_premature(jwt.decode(tokenSet.id_token)); @@ -249,12 +270,32 @@ function tokenset_is_premature(tokenSet) { return false; } +function token_is_premature(token) { + return !!(token.nbf && token.nbf < Date.now() / 1000); +} + function tokenset_issuer_match(tokenSet, issuer) { if (tokenSet.id_token) { return token_issuer_match(jwt.decode(tokenSet.id_token), issuer); } - return true; + return false; +} + +function token_issuer_match(token, issuer) { + return token.iss && token.iss == issuer; +} + +function tokenset_audience_match(tokenSet, audience) { + if (tokenSet.id_token) { + return token_audience_match(jwt.decode(tokenSet.id_token), audience); + } + + return false; +} + +function token_audience_match(token, audience) { + return token.aud && token.aud == audience; } function tokenset_can_refresh(tokenSet) { @@ -1660,7 +1701,7 @@ class BaseOauthPlugin extends BasePlugin { } // TODO: must verify the id_token here as it cannot be trusted otherwise - // currently this is left to the user by enabling assertions.id_token_signature + // currently this is left to the user by enabling assertions.sig.enabled if (clientProvidedTokenSet && !tokenSet.id_token) { //jwt.verify(tokenSet.id_token, "foo"); // TODO: in case of failure return what statusCode? 403 @@ -2614,15 +2655,22 @@ class BaseOauthPlugin extends BasePlugin { const pluginStrategy = plugin.get_plugin_strategy(); + let idToken; + if (pluginStrategy == PLUGIN_STRATEGY_OIDC && tokenSet.id_token) { + idToken = jwt.decode(tokenSet.id_token); + } + /** * token aud is the client_id */ if ( pluginStrategy == PLUGIN_STRATEGY_OIDC && - plugin.config.assertions.aud && - idToken.aud != plugin.config.client.client_id + plugin.config.assertions.aud ) { - return false; + if (!tokenset_audience_match(tokenSet, plugin.config.client.client_id)) { + plugin.server.logger.verbose("tokenSet audience mismatch"); + return false; + } } /** @@ -2647,8 +2695,8 @@ class BaseOauthPlugin extends BasePlugin { */ if ( plugin.config.assertions.exp && - tokenset_is_expired(tokenSet) && plugin.config.features.refresh_access_token && + tokenset_is_expired(tokenSet) && !tokenset_can_refresh(tokenSet) ) { plugin.server.logger.verbose( @@ -2717,8 +2765,6 @@ class BaseOauthPlugin extends BasePlugin { plugin.config.assertions.id_token && plugin.config.assertions.id_token.length > 0 ) { - let idToken; - idToken = jwt.decode(tokenSet.id_token); let idTokenValid = await plugin.id_token_assertions(idToken); if (!idTokenValid) { return false; @@ -2739,32 +2785,15 @@ class BaseOauthPlugin extends BasePlugin { if ( pluginStrategy == PLUGIN_STRATEGY_OIDC && - _.get(plugin.config, "assertions.id_token_signature.enabled", false) + _.get(plugin.config, "assertions.sig.enabled", false) ) { plugin.server.logger.debug("verifying id_token signature"); - async function getKey(header, callback) { - try { - let secret = _.get( - plugin.config, - "assertions.id_token_signature.key", - issuer.jwks_uri - ); - let key = await utils.get_jwt_sign_secret(secret, header.kid); - callback(null, key); - } catch (err) { - callback(err, null); - } - } + + let secret = _.get(plugin.config, "assertions.sig.secret", issuer.jwks_uri); try { - await new Promise((resolve, reject) => { - jwt.verify(tokenSet.id_token, getKey, {}, (err, decoded) => { - if (err) { - reject(err); - } - resolve(decoded); - }); - }); + // resolves to decoded token, if fails will go into catch + await tokenset_signature_is_trusted(tokenSet, secret); } catch (err) { plugin.server.logger.verbose(`id_token signature check failed: ${err}`); return false; diff --git a/src/server.js b/src/server.js index fd5ced2..e44b4ef 100644 --- a/src/server.js +++ b/src/server.js @@ -257,12 +257,8 @@ _verifyHandler = async (req, res, options = {}) => { configToken ); - // allow unsigned tokens - if (externalAuthServer.utils.is_json_like(configToken)) { - } else { - } - let configTokenLoaded = false; + // allow unsigned tokens if (process.env.EAS_ALLOW_PLAIN_SERVER_SIDE_TOKENS) { if ( externalAuthServer.utils.is_json_like(configToken) ||