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.fapi1() ? { 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), ...(this.fapi2() ? { id_token_signed_response_alg: 'PS256', authorization_signed_response_alg: 'PS256', 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'); } } if (this.fapi2()) { if ( properties.tls_client_certificate_bound_access_tokens && properties.dpop_bound_access_tokens ) { throw new TypeError( 'either tls_client_certificate_bound_access_tokens or dpop_bound_access_tokens must be set to true', ); } if ( !properties.tls_client_certificate_bound_access_tokens && !properties.dpop_bound_access_tokens ) { throw new TypeError( 'either tls_client_certificate_bound_access_tokens or dpop_bound_access_tokens must be set to true', ); } } 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 `