diff --git a/opencti-platform/opencti-graphql/package.json b/opencti-platform/opencti-graphql/package.json index bb68df2d3dcc7..062aa27c492d5 100644 --- a/opencti-platform/opencti-graphql/package.json +++ b/opencti-platform/opencti-graphql/package.json @@ -122,7 +122,7 @@ "node-fetch": "3.3.2", "nodemailer": "6.9.11", "openai": "4.28.4", - "openid-client": "5.6.4", + "openid-client": "5.6.5", "opentelemetry-node-metrics": "3.0.0", "otplib": "12.0.1", "passport": "0.7.0", @@ -208,5 +208,8 @@ "domino": "patch:domino@2.1.6#./patch/domino-2.1.6.patch", "graphql": "patch:graphql@16.8.1#./patch/graphql-16.8.1.patch" }, + "workspaces": [ + "packages/*" + ], "packageManager": "yarn@4.1.0" } diff --git a/opencti-platform/opencti-graphql/packages/node-openid-client/lib/client.js b/opencti-platform/opencti-graphql/packages/node-openid-client/lib/client.js new file mode 100644 index 0000000000000..dc8f927c70367 --- /dev/null +++ b/opencti-platform/opencti-graphql/packages/node-openid-client/lib/client.js @@ -0,0 +1,1849 @@ +const { inspect } = require('util'); +const stdhttp = require('http'); +const crypto = require('crypto'); +const { strict: assert } = require('assert'); +const querystring = require('querystring'); +const url = require('url'); +const { URL, URLSearchParams } = require('url'); + +const jose = require('jose'); +const tokenHash = require('oidc-token-hash'); + +const isKeyObject = require('./helpers/is_key_object'); +const decodeJWT = require('./helpers/decode_jwt'); +const base64url = require('./helpers/base64url'); +const defaults = require('./helpers/defaults'); +const parseWwwAuthenticate = require('./helpers/www_authenticate_parser'); +const { assertSigningAlgValuesSupport, assertIssuerConfiguration } = require('./helpers/assert'); +const pick = require('./helpers/pick'); +const isPlainObject = require('./helpers/is_plain_object'); +const processResponse = require('./helpers/process_response'); +const TokenSet = require('./token_set'); +const { OPError, RPError } = require('./errors'); +const now = require('./helpers/unix_timestamp'); +const { random } = require('./helpers/generators'); +const request = require('./helpers/request'); +const { CLOCK_TOLERANCE } = require('./helpers/consts'); +const { keystores } = require('./helpers/weak_cache'); +const KeyStore = require('./helpers/keystore'); +const clone = require('./helpers/deep_clone'); +const { authenticatedPost, resolveResponseType, resolveRedirectUri } = require('./helpers/client'); +const { queryKeyStore } = require('./helpers/issuer'); +const DeviceFlowHandle = require('./device_flow_handle'); + +const [major, minor] = process.version + .slice(1) + .split('.') + .map((str) => parseInt(str, 10)); + +const rsaPssParams = major >= 17 || (major === 16 && minor >= 9); +const retryAttempt = Symbol(); +const skipNonceCheck = Symbol(); +const skipMaxAgeCheck = Symbol(); + +function pickCb(input) { + return pick( + input, + 'access_token', // OAuth 2.0 + 'code', // OAuth 2.0 + 'error_description', // OAuth 2.0 + 'error_uri', // OAuth 2.0 + 'error', // OAuth 2.0 + 'expires_in', // OAuth 2.0 + 'id_token', // OIDC Core 1.0 + 'iss', // draft-ietf-oauth-iss-auth-resp + 'response', // FAPI JARM + 'session_state', // OIDC Session Management + 'state', // OAuth 2.0 + 'token_type', // OAuth 2.0 + ); +} + +function authorizationHeaderValue(token, tokenType = 'Bearer') { + return `${tokenType} ${token}`; +} + +function getSearchParams(input) { + const parsed = url.parse(input); + if (!parsed.search) return {}; + return querystring.parse(parsed.search.substring(1)); +} + +function verifyPresence(payload, jwt, prop) { + if (payload[prop] === undefined) { + throw new RPError({ + message: `missing required JWT property ${prop}`, + jwt, + }); + } +} + +function authorizationParams(params) { + const authParams = { + client_id: this.client_id, + scope: 'openid', + response_type: resolveResponseType.call(this), + redirect_uri: resolveRedirectUri.call(this), + ...params, + }; + + Object.entries(authParams).forEach(([key, value]) => { + if (value === null || value === undefined) { + delete authParams[key]; + } else if (key === 'claims' && typeof value === 'object') { + authParams[key] = JSON.stringify(value); + } else if (key === 'resource' && Array.isArray(value)) { + authParams[key] = value; + } else if (typeof value !== 'string') { + authParams[key] = String(value); + } + }); + + return authParams; +} + +function getKeystore(jwks) { + if ( + !isPlainObject(jwks) || + !Array.isArray(jwks.keys) || + jwks.keys.some((k) => !isPlainObject(k) || !('kty' in k)) + ) { + throw new TypeError('jwks must be a JSON Web Key Set formatted object'); + } + + return KeyStore.fromJWKS(jwks, { onlyPrivate: true }); +} + +// if an OP doesnt support client_secret_basic but supports client_secret_post, use it instead +// this is in place to take care of most common pitfalls when first using discovered Issuers without +// the support for default values defined by Discovery 1.0 +function checkBasicSupport(client, properties) { + try { + const supported = client.issuer.token_endpoint_auth_methods_supported; + if (!supported.includes(properties.token_endpoint_auth_method)) { + if (supported.includes('client_secret_post')) { + properties.token_endpoint_auth_method = 'client_secret_post'; + } + } + } catch (err) {} +} + +function handleCommonMistakes(client, metadata, properties) { + if (!metadata.token_endpoint_auth_method) { + // if no explicit value was provided + checkBasicSupport(client, properties); + } + + // :fp: c'mon people... RTFM + if (metadata.redirect_uri) { + if (metadata.redirect_uris) { + throw new TypeError('provide a redirect_uri or redirect_uris, not both'); + } + properties.redirect_uris = [metadata.redirect_uri]; + delete properties.redirect_uri; + } + + if (metadata.response_type) { + if (metadata.response_types) { + throw new TypeError('provide a response_type or response_types, not both'); + } + properties.response_types = [metadata.response_type]; + delete properties.response_type; + } +} + +function getDefaultsForEndpoint(endpoint, issuer, properties) { + if (!issuer[`${endpoint}_endpoint`]) return; + + const tokenEndpointAuthMethod = properties.token_endpoint_auth_method; + const tokenEndpointAuthSigningAlg = properties.token_endpoint_auth_signing_alg; + + const eam = `${endpoint}_endpoint_auth_method`; + const easa = `${endpoint}_endpoint_auth_signing_alg`; + + if (properties[eam] === undefined && properties[easa] === undefined) { + if (tokenEndpointAuthMethod !== undefined) { + properties[eam] = tokenEndpointAuthMethod; + } + if (tokenEndpointAuthSigningAlg !== undefined) { + properties[easa] = tokenEndpointAuthSigningAlg; + } + } +} + +class BaseClient { + #metadata; + #issuer; + #aadIssValidation; + #additionalAuthorizedParties; + constructor(issuer, aadIssValidation, metadata = {}, jwks, options) { + this.#metadata = new Map(); + this.#issuer = issuer; + this.#aadIssValidation = aadIssValidation; + + if (typeof metadata.client_id !== 'string' || !metadata.client_id) { + throw new TypeError('client_id is required'); + } + + const properties = { + grant_types: ['authorization_code'], + id_token_signed_response_alg: 'RS256', + authorization_signed_response_alg: 'RS256', + response_types: ['code'], + token_endpoint_auth_method: 'client_secret_basic', + ...(this.fapi() + ? { + grant_types: ['authorization_code', 'implicit'], + id_token_signed_response_alg: 'PS256', + authorization_signed_response_alg: 'PS256', + response_types: ['code id_token'], + tls_client_certificate_bound_access_tokens: true, + token_endpoint_auth_method: undefined, + } + : undefined), + ...metadata, + }; + + if (this.fapi()) { + switch (properties.token_endpoint_auth_method) { + case 'self_signed_tls_client_auth': + case 'tls_client_auth': + break; + case 'private_key_jwt': + if (!jwks) { + throw new TypeError('jwks is required'); + } + break; + case undefined: + throw new TypeError('token_endpoint_auth_method is required'); + default: + throw new TypeError('invalid or unsupported token_endpoint_auth_method'); + } + } + + handleCommonMistakes(this, metadata, properties); + + assertSigningAlgValuesSupport('token', this.issuer, properties); + ['introspection', 'revocation'].forEach((endpoint) => { + getDefaultsForEndpoint(endpoint, this.issuer, properties); + assertSigningAlgValuesSupport(endpoint, this.issuer, properties); + }); + + Object.entries(properties).forEach(([key, value]) => { + this.#metadata.set(key, value); + if (!this[key]) { + Object.defineProperty(this, key, { + get() { + return this.#metadata.get(key); + }, + enumerable: true, + }); + } + }); + + if (jwks !== undefined) { + const keystore = getKeystore.call(this, jwks); + keystores.set(this, keystore); + } + + if (options != null && options.additionalAuthorizedParties) { + this.#additionalAuthorizedParties = clone(options.additionalAuthorizedParties); + } + + this[CLOCK_TOLERANCE] = 0; + } + + authorizationUrl(params = {}) { + if (!isPlainObject(params)) { + throw new TypeError('params must be a plain object'); + } + assertIssuerConfiguration(this.issuer, 'authorization_endpoint'); + const target = new URL(this.issuer.authorization_endpoint); + + for (const [name, value] of Object.entries(authorizationParams.call(this, params))) { + if (Array.isArray(value)) { + target.searchParams.delete(name); + for (const member of value) { + target.searchParams.append(name, member); + } + } else { + target.searchParams.set(name, value); + } + } + + // TODO: is the replace needed? + return target.href.replace(/\+/g, '%20'); + } + + authorizationPost(params = {}) { + if (!isPlainObject(params)) { + throw new TypeError('params must be a plain object'); + } + const inputs = authorizationParams.call(this, params); + const formInputs = Object.keys(inputs) + .map((name) => ``) + .join('\n'); + + return ` +
+