From a9d3a87d2727bb37a535aeac9da9851ffdef8613 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 3 Oct 2023 16:47:38 +0200 Subject: [PATCH] feat: experimental Bun support This attempts to work around: - missing Node.js APIs in Bun - Bun's bugs in url.parse(..., true) - Bun loading `jose`'s bun target instead of the require one It is not possible to run openid-client's test suite due to other Bun Node.js compatibility bugs which is why this is "experimental" Refs #622 Refs #623 --- docs/README.md | 42 ++- lib/client.js | 280 +++++++++++-------- lib/helpers/client.js | 2 +- lib/helpers/keystore.js | 56 ++-- package-lock.json | 166 ++++++------ package.json | 13 +- test/client/client_instance.test.js | 340 ++++++++++++------------ test/client/dpop.test.js | 268 ++++++++++--------- test/client/implicit_kid.test.js | 4 +- test/client/mtls.test.js | 8 +- test/client/register_client.test.js | 10 +- test/client/self_issued.test.js | 56 ++-- test/issuer/issuer_instance.test.js | 10 +- test/keystore.js | 97 +++++++ test/passport/passport_strategy.test.js | 2 +- 15 files changed, 756 insertions(+), 598 deletions(-) create mode 100644 test/keystore.js diff --git a/docs/README.md b/docs/README.md index c6b624da..9d6ada90 100644 --- a/docs/README.md +++ b/docs/README.md @@ -299,10 +299,9 @@ Performs the callback for Authorization Server's authorization response. - `clientAssertionPayload`: `` extra client assertion payload parameters to be sent as part of a client JWT assertion. This is only used when the client's `token_endpoint_auth_method` is either `client_secret_jwt` or `private_key_jwt`. - - `DPoP`: `` When provided the client will send a DPoP Proof JWT to the - Token Endpoint. The value must be a private key in the form of a crypto.KeyObject, or any - valid crypto.createPrivateKey input. The algorithm is determined[^dpop-exception] automatically - based on the type of key and the issuer metadata. + - `DPoP`: `` or `` When provided the client will send a DPoP Proof JWT to the + Token Endpoint. The DPoP Proof JWT's algorithm is determined[^dpop-exception] automatically based + on the type of key and the issuer metadata. - Returns: `Promise` Parsed token endpoint response as a TokenSet. Tip: If you're using pure @@ -324,10 +323,9 @@ Performs `refresh_token` grant type exchange. - `clientAssertionPayload`: `` extra client assertion payload parameters to be sent as part of a client JWT assertion. This is only used when the client's `token_endpoint_auth_method` is either `client_secret_jwt` or `private_key_jwt`. - - `DPoP`: `` When provided the client will send a DPoP Proof JWT to the - Token Endpoint. The value must be a private key in the form of a crypto.KeyObject, or any - valid crypto.createPrivateKey input. The algorithm is determined[^dpop-exception] automatically - based on the type of key and the issuer metadata. + - `DPoP`: `` or `` When provided the client will send a DPoP Proof JWT to the + Token Endpoint. The DPoP Proof JWT's algorithm is determined[^dpop-exception] automatically based + on the type of key and the issuer metadata. - Returns: `Promise` Parsed token endpoint response as a TokenSet. --- @@ -348,10 +346,9 @@ will also be checked to match the on in the TokenSet's ID Token. or the `token_type` property from a passed in TokenSet. - `params`: `` additional parameters to send with the userinfo request (as query string when GET, as x-www-form-urlencoded body when POST). - - `DPoP`: `` When provided the client will send a DPoP Proof JWT to the - Userinfo Endpoint. The value must be a private key in the form of a crypto.KeyObject, or any - valid crypto.createPrivateKey input. The algorithm is determined[^dpop-exception] automatically - based on the type of key and the issuer metadata. + - `DPoP`: `` or `` When provided the client will send a DPoP Proof JWT to the + Userinfo Endpoint. The DPoP Proof JWT's algorithm is determined[^dpop-exception] automatically based + on the type of key and the issuer metadata. - Returns: `Promise` Parsed userinfo response. --- @@ -369,10 +366,9 @@ Fetches an arbitrary resource with the provided Access Token in an Authorization - `method`: `` The HTTP method to use for the request. **Default:** 'GET' - `tokenType`: `` The token type as the Authorization Header scheme. **Default:** 'Bearer' or the `token_type` property from a passed in TokenSet. - - `DPoP`: `` When provided the client will send a DPoP Proof JWT to the - Userinfo Endpoint. The value must be a private key in the form of a crypto.KeyObject, or any - valid crypto.createPrivateKey input. The algorithm is determined[^dpop-exception] automatically - based on the type of key and the issuer metadata. + - `DPoP`: `` or `` When provided the client will send a DPoP Proof JWT to the + Userinfo Endpoint. The DPoP Proof JWT's algorithm is determined[^dpop-exception] automatically based + on the type of key and the issuer metadata. - Returns: `Promise` Response is a [Got Response](https://github.com/sindresorhus/got/tree/v11.8.0#response) with the `body` property being a `` @@ -390,10 +386,9 @@ Performs an arbitrary `grant_type` exchange at the `token_endpoint`. - `clientAssertionPayload`: `` extra client assertion payload parameters to be sent as part of a client JWT assertion. This is only used when the client's `token_endpoint_auth_method` is either `client_secret_jwt` or `private_key_jwt`. - - `DPoP`: `` When provided the client will send a DPoP Proof JWT to the - Token Endpoint. The value must be a private key in the form of a crypto.KeyObject, or any - valid crypto.createPrivateKey input. The algorithm is determined[^dpop-exception] automatically - based on the type of key and the issuer metadata. + - `DPoP`: `` or `` When provided the client will send a DPoP Proof JWT to the + Token Endpoint. The DPoP Proof JWT's algorithm is determined[^dpop-exception] automatically based + on the type of key and the issuer metadata. - Returns: `Promise` --- @@ -467,10 +462,9 @@ a handle for subsequent Device Access Token Request polling. - `clientAssertionPayload`: `` extra client assertion payload parameters to be sent as part of a client JWT assertion. This is only used when the client's `token_endpoint_auth_method` is either `client_secret_jwt` or `private_key_jwt`. - - `DPoP`: `` When provided the client will send a DPoP Proof JWT to the - Token Endpoint. The value must be a private key in the form of a crypto.KeyObject, or any - valid crypto.createPrivateKey input. The algorithm is determined[^dpop-exception] automatically - based on the type of key and the issuer metadata. + - `DPoP`: `` or `` When provided the client will send a DPoP Proof JWT to the + Token Endpoint. The DPoP Proof JWT's algorithm is determined[^dpop-exception] automatically based + on the type of key and the issuer metadata. - Returns: `Promise` --- diff --git a/lib/client.js b/lib/client.js index 069f9b56..25431579 100644 --- a/lib/client.js +++ b/lib/client.js @@ -4,6 +4,7 @@ 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'); @@ -62,6 +63,12 @@ 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({ @@ -251,13 +258,21 @@ class BaseClient { throw new TypeError('params must be a plain object'); } assertIssuerConfiguration(this.issuer, 'authorization_endpoint'); - const target = url.parse(this.issuer.authorization_endpoint, true); - target.search = null; - target.query = { - ...target.query, - ...authorizationParams.call(this, params), - }; - return url.format(target); + 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('+', '%20'); } authorizationPost(params = {}) { @@ -297,10 +312,9 @@ class BaseClient { id_token_hint = id_token_hint.id_token; } - const target = url.parse(this.issuer.end_session_endpoint, true); - target.search = null; - defaults( - target.query, + const target = url.parse(this.issuer.end_session_endpoint); + const query = defaults( + getSearchParams(this.issuer.end_session_endpoint), params, { post_logout_redirect_uri, @@ -309,12 +323,15 @@ class BaseClient { { id_token_hint }, ); - Object.entries(target.query).forEach(([key, value]) => { + Object.entries(query).forEach(([key, value]) => { if (value === null || value === undefined) { - delete target.query[key]; + delete query[key]; } }); + target.search = null; + target.query = query; + return url.format(target); } @@ -331,7 +348,7 @@ class BaseClient { if (isIncomingMessage) { switch (input.method) { case 'GET': - return pickCb(url.parse(input.url, true).query); + return pickCb(getSearchParams(input.url)); case 'POST': if (input.body === undefined) { throw new TypeError( @@ -356,7 +373,7 @@ class BaseClient { throw new TypeError('invalid IncomingMessage method'); } } else { - return pickCb(url.parse(input, true).query); + return pickCb(getSearchParams(input)); } } @@ -703,11 +720,15 @@ class BaseClient { if (expectedAlg.match(/^(?:RSA|ECDH)/)) { const keystore = await keystores.get(this); - for (const { keyObject: key } of keystore.all({ - ...jose.decodeProtectedHeader(jwe), + const protectedHeader = jose.decodeProtectedHeader(jwe); + + for (const key of keystore.all({ + ...protectedHeader, use: 'enc', })) { - plaintext = await jose.compactDecrypt(jwe, key).then(getPlaintext, () => {}); + plaintext = await jose + .compactDecrypt(jwe, await key.keyObject(protectedHeader.alg)) + .then(getPlaintext, () => {}); if (plaintext) break; } } else { @@ -1016,7 +1037,13 @@ class BaseClient { assert(isPlainObject(payload.sub_jwk)); const key = await jose.importJWK(payload.sub_jwk, header.alg); assert.equal(key.type, 'public'); - keys = [{ keyObject: key }]; + keys = [ + { + keyObject() { + return key; + }, + }, + ]; } catch (err) { throw new RPError({ message: 'failed to use sub_jwk claim as an asymmetric JSON Web Key', @@ -1041,7 +1068,7 @@ class BaseClient { for (const key of keys) { const verified = await jose - .compactVerify(jwt, key instanceof Uint8Array ? key : key.keyObject) + .compactVerify(jwt, key instanceof Uint8Array ? key : await key.keyObject(header.alg)) .catch(() => {}); if (verified) { return { @@ -1195,12 +1222,12 @@ class BaseClient { targetUrl = this.issuer.mtls_endpoint_aliases.userinfo_endpoint; } - targetUrl = new url.URL(targetUrl || this.issuer.userinfo_endpoint); + targetUrl = new URL(targetUrl || this.issuer.userinfo_endpoint); if (via === 'body') { options.headers.Authorization = undefined; options.headers['Content-Type'] = 'application/x-www-form-urlencoded'; - options.body = new url.URLSearchParams(); + options.body = new URLSearchParams(); options.body.append( 'access_token', accessToken instanceof TokenSet ? accessToken.access_token : accessToken, @@ -1220,7 +1247,7 @@ class BaseClient { }); } else { // POST && via header - options.body = new url.URLSearchParams(); + options.body = new URLSearchParams(); options.headers['Content-Type'] = 'application/x-www-form-urlencoded'; Object.entries(params).forEach(([key, value]) => { options.body.append(key, value); @@ -1515,7 +1542,7 @@ class BaseClient { ...header, kid: symmetric ? undefined : key.jwk.kid, }) - .sign(symmetric ? key : key.keyObject); + .sign(symmetric ? key : await key.keyObject(signingAlgorithm)); } if (!eKeyManagement) { @@ -1539,7 +1566,7 @@ class BaseClient { ...fields, kid: key instanceof Uint8Array ? undefined : key.jwk.kid, }) - .encrypt(key instanceof Uint8Array ? key : key.keyObject); + .encrypt(key instanceof Uint8Array ? key : await key.keyObject(fields.alg)); } async pushedAuthorizationRequest(params = {}, { clientAssertionPayload } = {}) { @@ -1625,33 +1652,18 @@ class BaseClient { let privateKey; if (isKeyObject(privateKeyInput)) { privateKey = privateKeyInput; - } else { + } else if (privateKeyInput[Symbol.toStringTag] === 'CryptoKey') { + privateKey = privateKeyInput; + } else if (jose.cryptoRuntime === 'node:crypto') { privateKey = crypto.createPrivateKey(privateKeyInput); + } else { + throw new TypeError('unrecognized crypto runtime'); } if (privateKey.type !== 'private') { throw new TypeError('"DPoP" option must be a private key'); } - let alg; - switch (privateKey.asymmetricKeyType) { - case 'ed25519': - case 'ed448': - alg = 'EdDSA'; - break; - case 'ec': - alg = determineEcAlgorithm(privateKey, privateKeyInput); - break; - case 'rsa': - case rsaPssParams && 'rsa-pss': - alg = determineRsaAlgorithm( - privateKey, - privateKeyInput, - this.issuer.dpop_signing_alg_values_supported, - ); - break; - default: - throw new TypeError('unsupported DPoP private key asymmetric key type'); - } + let alg = determineDPoPAlgorithm.call(this, privateKey, privateKeyInput); if (!alg) { throw new TypeError('could not determine DPoP JWS Algorithm'); @@ -1674,81 +1686,138 @@ class BaseClient { } } -const RSPS = /^(?:RS|PS)(?:256|384|512)$/; -function determineRsaAlgorithm(privateKey, privateKeyInput, valuesSupported) { - if ( - typeof privateKeyInput === 'object' && - typeof privateKeyInput.key === 'object' && - privateKeyInput.key.alg - ) { - return privateKeyInput.key.alg; +function determineDPoPAlgorithmFromCryptoKey(cryptoKey) { + switch (cryptoKey.algorithm.name) { + case 'Ed25519': + case 'Ed448': + return 'EdDSA'; + case 'ECDSA': { + switch (cryptoKey.algorithm.namedCurve) { + case 'P-256': + return 'ES256'; + case 'P-384': + return 'ES384'; + case 'P-521': + return 'ES512'; + default: + break; + } + break; + } + case 'RSASSA-PKCS1-v1_5': + return `RS${cryptoKey.algorithm.hash.name.slice(4)}`; + case 'RSA-PSS': + return `PS${cryptoKey.algorithm.hash.name.slice(4)}`; + default: + throw new TypeError('unsupported DPoP private key'); } +} - if (Array.isArray(valuesSupported)) { - let candidates = valuesSupported.filter(RegExp.prototype.test.bind(RSPS)); - if (privateKey.asymmetricKeyType === 'rsa-pss') { - candidates = candidates.filter((value) => value.startsWith('PS')); +let determineDPoPAlgorithm; +if (jose.cryptoRuntime === 'node:crypto') { + determineDPoPAlgorithm = function (privateKey, privateKeyInput) { + if (privateKeyInput[Symbol.toStringTag] === 'CryptoKey') { + return determineDPoPAlgorithmFromCryptoKey(privateKey); } - return ['PS256', 'PS384', 'PS512', 'RS256', 'RS384', 'RS384'].find((preferred) => - candidates.includes(preferred), - ); - } - return 'PS256'; -} + switch (privateKey.asymmetricKeyType) { + case 'ed25519': + case 'ed448': + return 'EdDSA'; + case 'ec': + return determineEcAlgorithm(privateKey, privateKeyInput); + case 'rsa': + case rsaPssParams && 'rsa-pss': + return determineRsaAlgorithm( + privateKey, + privateKeyInput, + this.issuer.dpop_signing_alg_values_supported, + ); + default: + throw new TypeError('unsupported DPoP private key'); + } + }; -const p256 = Buffer.from([42, 134, 72, 206, 61, 3, 1, 7]); -const p384 = Buffer.from([43, 129, 4, 0, 34]); -const p521 = Buffer.from([43, 129, 4, 0, 35]); -const secp256k1 = Buffer.from([43, 129, 4, 0, 10]); + const RSPS = /^(?:RS|PS)(?:256|384|512)$/; + function determineRsaAlgorithm(privateKey, privateKeyInput, valuesSupported) { + if ( + typeof privateKeyInput === 'object' && + privateKeyInput.format === 'jwk' && + privateKeyInput.key && + privateKeyInput.key.alg + ) { + return privateKeyInput.key.alg; + } -function determineEcAlgorithm(privateKey, privateKeyInput) { - // If input was a JWK - switch ( - typeof privateKeyInput === 'object' && - typeof privateKeyInput.key === 'object' && - privateKeyInput.key.crv - ) { - case 'P-256': + if (Array.isArray(valuesSupported)) { + let candidates = valuesSupported.filter(RegExp.prototype.test.bind(RSPS)); + if (privateKey.asymmetricKeyType === 'rsa-pss') { + candidates = candidates.filter((value) => value.startsWith('PS')); + } + return ['PS256', 'PS384', 'PS512', 'RS256', 'RS384', 'RS384'].find((preferred) => + candidates.includes(preferred), + ); + } + + return 'PS256'; + } + + const p256 = Buffer.from([42, 134, 72, 206, 61, 3, 1, 7]); + const p384 = Buffer.from([43, 129, 4, 0, 34]); + const p521 = Buffer.from([43, 129, 4, 0, 35]); + const secp256k1 = Buffer.from([43, 129, 4, 0, 10]); + + function determineEcAlgorithm(privateKey, privateKeyInput) { + // If input was a JWK + switch ( + typeof privateKeyInput === 'object' && + typeof privateKeyInput.key === 'object' && + privateKeyInput.key.crv + ) { + case 'P-256': + return 'ES256'; + case 'secp256k1': + return 'ES256K'; + case 'P-384': + return 'ES384'; + case 'P-512': + return 'ES512'; + default: + break; + } + + const buf = privateKey.export({ format: 'der', type: 'pkcs8' }); + const i = buf[1] < 128 ? 17 : 18; + const len = buf[i]; + const curveOid = buf.slice(i + 1, i + 1 + len); + if (curveOid.equals(p256)) { return 'ES256'; - case 'secp256k1': - return 'ES256K'; - case 'P-384': + } + + if (curveOid.equals(p384)) { return 'ES384'; - case 'P-512': + } + if (curveOid.equals(p521)) { return 'ES512'; - default: - break; - } - - const buf = privateKey.export({ format: 'der', type: 'pkcs8' }); - const i = buf[1] < 128 ? 17 : 18; - const len = buf[i]; - const curveOid = buf.slice(i + 1, i + 1 + len); - if (curveOid.equals(p256)) { - return 'ES256'; - } + } - if (curveOid.equals(p384)) { - return 'ES384'; - } - if (curveOid.equals(p521)) { - return 'ES512'; - } + if (curveOid.equals(secp256k1)) { + return 'ES256K'; + } - if (curveOid.equals(secp256k1)) { - return 'ES256K'; + throw new TypeError('unsupported DPoP private key curve'); } - - throw new TypeError('unsupported DPoP private key curve'); +} else { + determineDPoPAlgorithm = determineDPoPAlgorithmFromCryptoKey; } const jwkCache = new WeakMap(); -async function getJwk(privateKey, privateKeyInput) { +async function getJwk(keyObject, privateKeyInput) { if ( + jose.cryptoRuntime === 'node:crypto' && typeof privateKeyInput === 'object' && typeof privateKeyInput.key === 'object' && - privateKeyInput.key.crv + privateKeyInput.format === 'jwk' ) { return pick(privateKeyInput.key, 'kty', 'crv', 'x', 'y', 'e', 'n'); } @@ -1757,9 +1826,9 @@ async function getJwk(privateKey, privateKeyInput) { return jwkCache.get(privateKeyInput); } - const jwk = pick(await jose.exportJWK(privateKey), 'kty', 'crv', 'x', 'y', 'e', 'n'); + const jwk = pick(await jose.exportJWK(keyObject), 'kty', 'crv', 'x', 'y', 'e', 'n'); - if (isKeyObject(privateKeyInput)) { + if (isKeyObject(privateKeyInput) || jose.cryptoRuntime === 'WebCryptoAPI') { jwkCache.set(privateKeyInput, jwk); } @@ -1776,4 +1845,5 @@ module.exports = (issuer, aadIssValidation = false) => return issuer; } }; + module.exports.BaseClient = BaseClient; diff --git a/lib/helpers/client.js b/lib/helpers/client.js index 7c002482..8c2f7fc7 100644 --- a/lib/helpers/client.js +++ b/lib/helpers/client.js @@ -70,7 +70,7 @@ async function clientAssertion(endpoint, payload) { return new jose.CompactSign(Buffer.from(JSON.stringify(payload))) .setProtectedHeader({ alg, kid: key.jwk && key.jwk.kid }) - .sign(key.keyObject); + .sign(await key.keyObject(alg)); } async function authFor(endpoint, { clientAssertionPayload } = {}) { diff --git a/lib/helpers/keystore.js b/lib/helpers/keystore.js index e8bba155..6118430f 100644 --- a/lib/helpers/keystore.js +++ b/lib/helpers/keystore.js @@ -2,7 +2,6 @@ const jose = require('jose'); const clone = require('./deep_clone'); const isPlainObject = require('./is_plain_object'); -const isKeyObject = require('./is_key_object'); const internal = Symbol(); @@ -51,7 +50,7 @@ function getKtyFromAlg(alg) { function getAlgorithms(use, alg, kty, crv) { // Ed25519, Ed448, and secp256k1 always have "alg" - // OKP always has use + // OKP always has "use" if (alg) { return new Set([alg]); } @@ -65,7 +64,20 @@ function getAlgorithms(use, alg, kty, crv) { } if (use === 'sig' || use === undefined) { - algs = algs.concat([`ES${crv.slice(-3)}`.replace('21', '12')]); + switch (crv) { + case 'P-256': + case 'P-384': + algs = algs.concat([`ES${crv.slice(-3)}`.replace('21', '12')]); + break; + case 'P-521': + algs = algs.concat(['ES512']); + break; + case 'secp256k1': + if (jose.cryptoRuntime === 'node:crypto') { + algs = algs.concat(['ES256K']); + } + break; + } } return new Set(algs); @@ -77,7 +89,10 @@ function getAlgorithms(use, alg, kty, crv) { let algs = []; if (use === 'enc' || use === undefined) { - algs = algs.concat(['RSA-OAEP', 'RSA-OAEP-256', 'RSA-OAEP-384', 'RSA-OAEP-512', 'RSA1_5']); + algs = algs.concat(['RSA-OAEP', 'RSA-OAEP-256', 'RSA-OAEP-384', 'RSA-OAEP-512']); + if (jose.cryptoRuntime === 'node:crypto') { + algs = algs.concat(['RSA1_5']); + } } if (use === 'sig' || use === undefined) { @@ -225,36 +240,25 @@ module.exports = class KeyStore { } } - const keyObject = await jose.importJWK(jwk, alg || fauxAlg(jwk.kty)).catch(() => {}); - - if (!keyObject) continue; - - if (keyObject instanceof Uint8Array || keyObject.type === 'secret') { - if (onlyPrivate) { - throw new Error('jwks must only contain private keys'); - } - continue; - } - - if (!isKeyObject(keyObject)) { - throw new Error('what?!'); - } - - if (onlyPrivate && keyObject.type !== 'private') { + if (onlyPrivate && (jwk.kty === 'oct' || !jwk.d)) { throw new Error('jwks must only contain private keys'); } - if (onlyPublic && keyObject.type !== 'public') { - continue; - } - - if (kty === 'RSA' && keyObject.asymmetricKeySize < 2048) { + if (onlyPublic && (jwk.d || jwk.k)) { continue; } keys.push({ jwk: { ...jwk, alg, use }, - keyObject, + async keyObject(alg) { + if (this[alg]) { + return this[alg]; + } + + const keyObject = await jose.importJWK(this.jwk, alg); + this[alg] = keyObject; + return keyObject; + }, get algorithms() { Object.defineProperty(this, 'algorithms', { value: getAlgorithms(this.jwk.use, this.jwk.alg, this.jwk.kty, this.jwk.crv), diff --git a/package-lock.json b/package-lock.json index ec142647..7cb111f8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,37 +9,27 @@ "version": "5.5.0", "license": "MIT", "dependencies": { - "jose": "^4.14.4", + "jose": "^4.15.1", "lru-cache": "^6.0.0", "object-hash": "^2.2.0", "oidc-token-hash": "^5.0.3" }, "devDependencies": { - "@types/node": "^16.18.31", - "@types/passport": "^1.0.12", + "@types/node": "^16.18.55", + "@types/passport": "^1.0.13", "base64url": "^3.0.1", - "chai": "^4.3.7", - "jose2": "npm:jose@^2.0.6", + "chai": "^4.3.10", "mocha": "^10.2.0", - "nock": "^13.3.1", + "nock": "^13.3.3", "prettier": "^2.8.8", "readable-mock-req": "^0.2.2", "sinon": "^9.2.4", - "timekeeper": "^2.2.0" + "timekeeper": "^2.3.1" }, "funding": { "url": "https://github.com/sponsors/panva" } }, - "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==", - "dev": true, - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/@sinonjs/commons": { "version": "1.8.6", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz", @@ -76,9 +66,9 @@ "dev": true }, "node_modules/@types/body-parser": { - "version": "1.19.2", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", - "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", + "version": "1.19.3", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.3.tgz", + "integrity": "sha512-oyl4jvAfTGX9Bt6Or4H9ni1Z447/tQuxnZsytsCaExKlmJiU8sFgnIBRzJUpKwB5eWn9HuBYlUlVA74q/yN0eQ==", "dev": true, "dependencies": { "@types/connect": "*", @@ -86,18 +76,18 @@ } }, "node_modules/@types/connect": { - "version": "3.4.35", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", - "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", + "version": "3.4.36", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.36.tgz", + "integrity": "sha512-P63Zd/JUGq+PdrM1lv0Wv5SBYeA2+CORvbrXbngriYY0jzLUWfQMQQxOhjONEz/wlHOAxOdY7CY65rgQdTjq2w==", "dev": true, "dependencies": { "@types/node": "*" } }, "node_modules/@types/express": { - "version": "4.17.17", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.17.tgz", - "integrity": "sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q==", + "version": "4.17.18", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.18.tgz", + "integrity": "sha512-Sxv8BSLLgsBYmcnGdGjjEjqET2U+AKAdCRODmMiq02FgjwuV75Ut85DRpvFjyw/Mk0vgUOliGRU0UUmuuZHByQ==", "dev": true, "dependencies": { "@types/body-parser": "*", @@ -107,9 +97,9 @@ } }, "node_modules/@types/express-serve-static-core": { - "version": "4.17.35", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.35.tgz", - "integrity": "sha512-wALWQwrgiB2AWTT91CB62b6Yt0sNHpznUXeZEcnPU3DRdlDIz74x8Qg1UUYKSVFi+va5vKOLYRBI1bRKiLLKIg==", + "version": "4.17.37", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.37.tgz", + "integrity": "sha512-ZohaCYTgGFcOP7u6aJOhY9uIZQgZ2vxC2yWoArY+FeDXlqeH66ZVBjgvg+RLVAS/DWNq4Ap9ZXu1+SUQiiWYMg==", "dev": true, "dependencies": { "@types/node": "*", @@ -118,43 +108,49 @@ "@types/send": "*" } }, + "node_modules/@types/http-errors": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.2.tgz", + "integrity": "sha512-lPG6KlZs88gef6aD85z3HNkztpj7w2R7HmR3gygjfXCQmsLloWNARFkMuzKiiY8FGdh1XDpgBdrSf4aKDiA7Kg==", + "dev": true + }, "node_modules/@types/mime": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", - "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.3.tgz", + "integrity": "sha512-Ys+/St+2VF4+xuY6+kDIXGxbNRO0mesVg0bbxEfB97Od1Vjpjx9KD1qxs64Gcb3CWPirk9Xe+PT4YiiHQ9T+eg==", "dev": true }, "node_modules/@types/node": { - "version": "16.18.31", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.31.tgz", - "integrity": "sha512-KPXltf4z4g517OlVJO9XQ2357CYw7fvuJ3ZuBynjXC5Jos9i+K7LvFb7bUIwtJXSZj0vTp9Q6NJBSQpkwwO8Zw==", + "version": "16.18.57", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.57.tgz", + "integrity": "sha512-piPoDozdPaX1hNWFJQzzgWqE40gh986VvVx/QO9RU4qYRE55ld7iepDVgZ3ccGUw0R4wge0Oy1dd+3xOQNkkUQ==", "dev": true }, "node_modules/@types/passport": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.12.tgz", - "integrity": "sha512-QFdJ2TiAEoXfEQSNDISJR1Tm51I78CymqcBa8imbjo6dNNu+l2huDxxbDEIoFIwOSKMkOfHEikyDuZ38WwWsmw==", + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.13.tgz", + "integrity": "sha512-XXURryL+EZAWtbQFOHX1eNB+RJwz5XMPPz1xrGpEKr2xUZCXM4NCPkHMtZQ3B2tTSG/1IRaAcTHjczRA4sSFCw==", "dev": true, "dependencies": { "@types/express": "*" } }, "node_modules/@types/qs": { - "version": "6.9.7", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", - "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==", + "version": "6.9.8", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.8.tgz", + "integrity": "sha512-u95svzDlTysU5xecFNTgfFG5RUWu1A9P0VzgpcIiGZA9iraHOdSzcxMxQ55DyeRaGCSxQi7LxXDI4rzq/MYfdg==", "dev": true }, "node_modules/@types/range-parser": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", - "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.5.tgz", + "integrity": "sha512-xrO9OoVPqFuYyR/loIHjnbvvyRZREYKLjxV4+dY6v3FQR3stQ9ZxIGkaclF7YhI9hfjpuTbu14hZEy94qKLtOA==", "dev": true }, "node_modules/@types/send": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.1.tgz", - "integrity": "sha512-Cwo8LE/0rnvX7kIIa3QHCkcuF21c05Ayb0ZfxPiv0W8VRiZiNW/WuRupHKpqqGVGf7SUA44QSOUKaEd9lIrd/Q==", + "version": "0.17.2", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.2.tgz", + "integrity": "sha512-aAG6yRf6r0wQ29bkS+x97BIs64ZLxeE/ARwyS6wrldMm3C1MdKwCcnnEwMC1slI8wuxJOpiUH9MioC0A0i+GJw==", "dev": true, "dependencies": { "@types/mime": "^1", @@ -162,11 +158,12 @@ } }, "node_modules/@types/serve-static": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.1.tgz", - "integrity": "sha512-NUo5XNiAdULrJENtJXZZ3fHtfMolzZwczzBbnAeBbqBwG+LaG6YaJtuwzwGSQZ2wsCrxjEhNNjAkKigy3n8teQ==", + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.3.tgz", + "integrity": "sha512-yVRvFsEMrv7s0lGhzrggJjNOSmZCdgCjw9xWrPr/kNNLp6FaDfMC1KaYl3TSJ0c58bECwNBMoQrZJ8hA8E1eFg==", "dev": true, "dependencies": { + "@types/http-errors": "*", "@types/mime": "*", "@types/node": "*" } @@ -296,18 +293,18 @@ } }, "node_modules/chai": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.7.tgz", - "integrity": "sha512-HLnAzZ2iupm25PlN0xFreAlBA5zaBSv3og0DdeGA4Ar6h6rJ3A0rolRUKJhSF2V10GZKDgWF/VmAEsNWjCRB+A==", + "version": "4.3.10", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.10.tgz", + "integrity": "sha512-0UXG04VuVbruMUYbJ6JctvH0YnC/4q3/AkT18q4NaITo91CUm0liMS9VqzT9vZhVQ/1eqPanMWjBM+Juhfb/9g==", "dev": true, "dependencies": { "assertion-error": "^1.1.0", - "check-error": "^1.0.2", - "deep-eql": "^4.1.2", - "get-func-name": "^2.0.0", - "loupe": "^2.3.1", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", "pathval": "^1.1.1", - "type-detect": "^4.0.5" + "type-detect": "^4.0.8" }, "engines": { "node": ">=4" @@ -342,10 +339,13 @@ } }, "node_modules/check-error": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", - "integrity": "sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", "dev": true, + "dependencies": { + "get-func-name": "^2.0.2" + }, "engines": { "node": "*" } @@ -545,9 +545,9 @@ "dev": true }, "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, "hasInstallScript": true, "optional": true, @@ -568,9 +568,9 @@ } }, "node_modules/get-func-name": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", - "integrity": "sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", "dev": true, "engines": { "node": "*" @@ -750,25 +750,9 @@ "dev": true }, "node_modules/jose": { - "version": "4.14.4", - "resolved": "https://registry.npmjs.org/jose/-/jose-4.14.4.tgz", - "integrity": "sha512-j8GhLiKmUAh+dsFXlX1aJCbt5KMibuKb+d7j1JaOJG6s2UjX1PQlW+OKB/sD4a/5ZYF4RcmYmLSndOoU3Lt/3g==", - "funding": { - "url": "https://github.com/sponsors/panva" - } - }, - "node_modules/jose2": { - "name": "jose", - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/jose/-/jose-2.0.6.tgz", - "integrity": "sha512-FVoPY7SflDodE4lknJmbAHSUjLCzE2H1F6MS0RYKMQ8SR+lNccpMf8R4eqkNYyyUjR5qZReOzZo5C5YiHOCjjg==", - "dev": true, - "dependencies": { - "@panva/asn1.js": "^1.0.0" - }, - "engines": { - "node": ">=10.13.0 < 13 || >=13.7.0" - }, + "version": "4.15.1", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.1.tgz", + "integrity": "sha512-CinpaEMmwb/59YG0N6SC3DY1imdTU5iNl08HPWR7NdyxACPeFuQbqjaocEjCDGq04KbnxSqQu702vL3ZTvKe5w==", "funding": { "url": "https://github.com/sponsors/panva" } @@ -953,9 +937,9 @@ } }, "node_modules/nock": { - "version": "13.3.1", - "resolved": "https://registry.npmjs.org/nock/-/nock-13.3.1.tgz", - "integrity": "sha512-vHnopocZuI93p2ccivFyGuUfzjq2fxNyNurp7816mlT5V5HF4SzXu8lvLrVzBbNqzs+ODooZ6OksuSUNM7Njkw==", + "version": "13.3.3", + "resolved": "https://registry.npmjs.org/nock/-/nock-13.3.3.tgz", + "integrity": "sha512-z+KUlILy9SK/RjpeXDiDUEAq4T94ADPHE3qaRkf66mpEhzc/ytOMm3Bwdrbq6k1tMWkbdujiKim3G2tfQARuJw==", "dev": true, "dependencies": { "debug": "^4.1.0", @@ -1287,9 +1271,9 @@ } }, "node_modules/timekeeper": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/timekeeper/-/timekeeper-2.2.0.tgz", - "integrity": "sha512-W3AmPTJWZkRwu+iSNxPIsLZ2ByADsOLbbLxe46UJyWj3mlYLlwucKiq+/dPm0l9wTzqoF3/2PH0AGFCebjq23A==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/timekeeper/-/timekeeper-2.3.1.tgz", + "integrity": "sha512-LeQRS7/4JcC0PgdSFnfUiStQEdiuySlCj/5SJ18D+T1n9BoY7PxKFfCwLulpHXoLUFr67HxBddQdEX47lDGx1g==", "dev": true }, "node_modules/to-regex-range": { diff --git a/package.json b/package.json index 1c5e9206..f67c67aa 100644 --- a/package.json +++ b/package.json @@ -45,23 +45,22 @@ "test": "mocha test/**/*.test.js" }, "dependencies": { - "jose": "^4.14.4", + "jose": "^4.15.1", "lru-cache": "^6.0.0", "object-hash": "^2.2.0", "oidc-token-hash": "^5.0.3" }, "devDependencies": { - "@types/node": "^16.18.31", - "@types/passport": "^1.0.12", + "@types/node": "^16.18.55", + "@types/passport": "^1.0.13", "base64url": "^3.0.1", - "chai": "^4.3.7", - "jose2": "npm:jose@^2.0.6", + "chai": "^4.3.10", "mocha": "^10.2.0", - "nock": "^13.3.1", + "nock": "^13.3.3", "prettier": "^2.8.8", "readable-mock-req": "^0.2.2", "sinon": "^9.2.4", - "timekeeper": "^2.2.0" + "timekeeper": "^2.3.1" }, "standard-version": { "scripts": { diff --git a/test/client/client_instance.test.js b/test/client/client_instance.test.js index dac0adbb..100ec71b 100644 --- a/test/client/client_instance.test.js +++ b/test/client/client_instance.test.js @@ -8,7 +8,7 @@ const { expect } = require('chai'); const base64url = require('base64url'); const nock = require('nock'); const sinon = require('sinon'); -const jose2 = require('jose2'); +const jose = require('jose'); const timekeeper = require('timekeeper'); const TokenSet = require('../../lib/token_set'); @@ -17,12 +17,19 @@ const now = require('../../lib/helpers/unix_timestamp'); const { Issuer, custom } = require('../../lib'); const clientInternal = require('../../lib/helpers/client'); const issuerInternal = require('../../lib/helpers/issuer'); +const KeyStore = require('../keystore'); const fail = () => { throw new Error('expected promise to be rejected'); }; const encode = (object) => base64url.encode(JSON.stringify(object)); +function getSearchParams(input) { + const parsed = url.parse(input); + if (!parsed.search) return {}; + return querystring.parse(parsed.search.substring(1)); +} + describe('Client', () => { afterEach(timekeeper.reset); afterEach(nock.cleanAll); @@ -57,12 +64,11 @@ describe('Client', () => { it('returns a string with the url with some basic defaults', function () { expect( - url.parse( + getSearchParams( this.client.authorizationUrl({ redirect_uri: 'https://rp.example.com/cb', }), - true, - ).query, + ), ).to.eql({ client_id: 'identifier', redirect_uri: 'https://rp.example.com/cb', @@ -73,12 +79,11 @@ describe('Client', () => { it('returns a string with the url and client meta specific defaults', function () { expect( - url.parse( + getSearchParams( this.clientWithMeta.authorizationUrl({ nonce: 'foo', }), - true, - ).query, + ), ).to.eql({ nonce: 'foo', client_id: 'identifier', @@ -89,7 +94,7 @@ describe('Client', () => { }); it('returns a string with the url and no defaults if client has more metas', function () { - expect(url.parse(this.clientWithMultipleMetas.authorizationUrl(), true).query).to.eql({ + expect(getSearchParams(this.clientWithMultipleMetas.authorizationUrl())).to.eql({ client_id: 'identifier', scope: 'openid', }); @@ -97,12 +102,11 @@ describe('Client', () => { it('keeps original query parameters', function () { expect( - url.parse( + getSearchParams( this.clientWithQuery.authorizationUrl({ redirect_uri: 'https://rp.example.com/cb', }), - true, - ).query, + ), ).to.eql({ client_id: 'identifier', redirect_uri: 'https://rp.example.com/cb', @@ -114,15 +118,14 @@ describe('Client', () => { it('allows to overwrite the defaults', function () { expect( - url.parse( + getSearchParams( this.client.authorizationUrl({ scope: 'openid offline_access', redirect_uri: 'https://rp.example.com/cb', response_type: 'id_token', nonce: 'foobar', }), - true, - ).query, + ), ).to.eql({ client_id: 'identifier', scope: 'openid offline_access', @@ -134,13 +137,12 @@ describe('Client', () => { it('allows any other params to be provide too', function () { expect( - url.parse( + getSearchParams( this.client.authorizationUrl({ state: 'state', custom: 'property', }), - true, - ).query, + ), ).to.contain({ state: 'state', custom: 'property', @@ -149,12 +151,11 @@ describe('Client', () => { it('allows resource to passed as an array', function () { expect( - url.parse( + getSearchParams( this.client.authorizationUrl({ resource: ['urn:example:com', 'urn:example-2:com'], }), - true, - ).query, + ), ).to.deep.contain({ resource: ['urn:example:com', 'urn:example-2:com'], }); @@ -162,12 +163,11 @@ describe('Client', () => { it('auto-stringifies claims parameter', function () { expect( - url.parse( + getSearchParams( this.client.authorizationUrl({ claims: { id_token: { email: null } }, }), - true, - ).query, + ), ).to.contain({ claims: '{"id_token":{"email":null}}', }); @@ -175,25 +175,23 @@ describe('Client', () => { it('removes null and undefined values', function () { expect( - url.parse( + getSearchParams( this.client.authorizationUrl({ state: null, prompt: undefined, }), - true, - ).query, + ), ).not.to.have.keys('state', 'prompt'); }); it('stringifies other values', function () { expect( - url.parse( + getSearchParams( this.client.authorizationUrl({ max_age: 300, foo: true, }), - true, - ).query, + ), ).to.contain({ max_age: '300', foo: 'true', @@ -251,7 +249,7 @@ describe('Client', () => { }); it('defaults the post_logout_redirect_uri if client has some', function () { - expect(url.parse(this.clientWithUris.endSessionUrl(), true).query).to.eql({ + expect(getSearchParams(this.clientWithUris.endSessionUrl())).to.eql({ client_id: 'identifier', post_logout_redirect_uri: 'https://rp.example.com/logout/cb', }); @@ -264,12 +262,11 @@ describe('Client', () => { access_token: 'tokenValue', }); expect( - url.parse( + getSearchParams( this.client.endSessionUrl({ id_token_hint: hint, }), - true, - ).query, + ), ).to.eql({ client_id: 'identifier', id_token_hint: 'eyJhbGciOiJub25lIn0.eyJzdWIiOiJzdWJqZWN0In0.', @@ -290,13 +287,12 @@ describe('Client', () => { it('allows to override default applied values', function () { expect( - url.parse( + getSearchParams( this.client.endSessionUrl({ post_logout_redirect_uri: 'override', client_id: 'override', }), - true, - ).query, + ), ).to.eql({ post_logout_redirect_uri: 'override', client_id: 'override', @@ -305,14 +301,13 @@ describe('Client', () => { it('allows for recommended and optional query params to be passed in', function () { expect( - url.parse( + getSearchParams( this.client.endSessionUrl({ post_logout_redirect_uri: 'https://rp.example.com/logout/cb', state: 'foo', id_token_hint: 'idtoken', }), - true, - ).query, + ), ).to.eql({ post_logout_redirect_uri: 'https://rp.example.com/logout/cb', state: 'foo', @@ -320,15 +315,14 @@ describe('Client', () => { client_id: 'identifier', }); expect( - url.parse( + getSearchParams( this.clientWithQuery.endSessionUrl({ post_logout_redirect_uri: 'https://rp.example.com/logout/cb', state: 'foo', id_token_hint: 'idtoken', foo: 'this will be ignored', }), - true, - ).query, + ), ).to.eql({ post_logout_redirect_uri: 'https://rp.example.com/logout/cb', state: 'foo', @@ -569,17 +563,15 @@ describe('Client', () => { authorization_signed_response_alg: 'HS256', }); - const response = jose2.JWT.sign( - { - code: 'foo', - }, - client.client_secret, - { - issuer: this.issuerWithIssResponse.issuer, - audience: client.client_id, - expiresIn: '5m', - }, - ); + const response = await new jose.SignJWT({ + code: 'foo', + iss: this.issuerWithIssResponse.issuer, + aud: client.client_id, + }) + .setIssuedAt() + .setExpirationTime('5m') + .setProtectedHeader({ alg: 'HS256' }) + .sign(new TextEncoder().encode(client.client_secret)); nock('https://op.example.com') .matchHeader('Accept', 'application/json') @@ -622,24 +614,24 @@ describe('Client', () => { authorization_encrypted_response_enc: 'A128GCM', }); - const response = jose2.JWE.encrypt( - jose2.JWT.sign( - { - code: 'foo', - }, - client.client_secret, - { - issuer: this.issuerWithIssResponse.issuer, - audience: client.client_id, - expiresIn: '5m', - }, - ), - await client.secretForAlg('A128GCM'), - { + const cleartext = new TextEncoder().encode( + await new jose.SignJWT({ + code: 'foo', + iss: this.issuerWithIssResponse.issuer, + aud: client.client_id, + }) + .setIssuedAt() + .setExpirationTime('5m') + .setProtectedHeader({ alg: 'HS256' }) + .sign(new TextEncoder().encode(client.client_secret)), + ); + + const response = await new jose.CompactEncrypt(cleartext) + .setProtectedHeader({ alg: 'dir', enc: 'A128GCM', - }, - ); + }) + .encrypt(await client.secretForAlg('A128GCM')); nock('https://op.example.com') .matchHeader('Accept', 'application/json') @@ -697,17 +689,15 @@ describe('Client', () => { authorization_signed_response_alg: 'HS256', }); - const response = jose2.JWT.sign( - { - code: 'foo', - }, - client.client_secret, - { - issuer: this.issuerWithIssResponse.issuer, - audience: client.client_id, - expiresIn: '5m', - }, - ); + const response = await new jose.SignJWT({ + code: 'foo', + iss: this.issuerWithIssResponse.issuer, + aud: client.client_id, + }) + .setIssuedAt() + .setProtectedHeader({ alg: 'HS256' }) + .setExpirationTime('5m') + .sign(new TextEncoder().encode(client.client_secret)); return this.client .callback( @@ -945,17 +935,15 @@ describe('Client', () => { authorization_signed_response_alg: 'HS256', }); - const response = jose2.JWT.sign( - { - code: 'foo', - }, - client.client_secret, - { - issuer: this.issuerWithIssResponse.issuer, - audience: client.client_id, - expiresIn: '5m', - }, - ); + const response = await new jose.SignJWT({ + code: 'foo', + iss: this.issuerWithIssResponse.issuer, + aud: client.client_id, + }) + .setIssuedAt() + .setProtectedHeader({ alg: 'HS256' }) + .setExpirationTime('5m') + .sign(new TextEncoder().encode(client.client_secret)); nock('https://op.example.com') .matchHeader('Accept', 'application/json') @@ -998,24 +986,24 @@ describe('Client', () => { authorization_encrypted_response_enc: 'A128GCM', }); - const response = jose2.JWE.encrypt( - jose2.JWT.sign( - { - code: 'foo', - }, - client.client_secret, - { - issuer: this.issuerWithIssResponse.issuer, - audience: client.client_id, - expiresIn: '5m', - }, - ), - await client.secretForAlg('A128GCM'), - { + const cleartext = new TextEncoder().encode( + await new jose.SignJWT({ + code: 'foo', + iss: this.issuerWithIssResponse.issuer, + aud: client.client_id, + }) + .setIssuedAt() + .setExpirationTime('5m') + .setProtectedHeader({ alg: 'HS256' }) + .sign(new TextEncoder().encode(client.client_secret)), + ); + + const response = await new jose.CompactEncrypt(cleartext) + .setProtectedHeader({ alg: 'dir', enc: 'A128GCM', - }, - ); + }) + .encrypt(await client.secretForAlg('A128GCM')); nock('https://op.example.com') .matchHeader('Accept', 'application/json') @@ -1073,17 +1061,15 @@ describe('Client', () => { authorization_signed_response_alg: 'HS256', }); - const response = jose2.JWT.sign( - { - code: 'foo', - }, - client.client_secret, - { - issuer: this.issuer.issuer, - audience: client.client_id, - expiresIn: '5m', - }, - ); + const response = await new jose.SignJWT({ + code: 'foo', + iss: this.issuer.issuer, + aud: client.client_id, + }) + .setIssuedAt() + .setProtectedHeader({ alg: 'HS256' }) + .setExpirationTime('5m') + .sign(new TextEncoder().encode(client.client_secret)); return this.client .oauthCallback( @@ -1379,63 +1365,57 @@ describe('Client', () => { }); }); - it('passes ID Token validations when ID Token is returned', function () { + it('passes ID Token validations when ID Token is returned', async function () { nock('https://op.example.com') .matchHeader('Accept', 'application/json') .post('/token') // to make sure filteringRequestBody works .reply(200, { access_token: 'present', refresh_token: 'refreshValue', - id_token: jose2.JWT.sign( - { - sub: 'foo', - }, - this.client.client_secret, - { - issuer: this.client.issuer.issuer, - audience: this.client.client_id, - expiresIn: '5m', - }, - ), + id_token: await new jose.SignJWT({ + sub: 'foo', + iss: this.client.issuer.issuer, + aud: this.client.client_id, + }) + .setIssuedAt() + .setProtectedHeader({ alg: 'HS256' }) + .setExpirationTime('5m') + .sign(new TextEncoder().encode(this.client.client_secret)), }); return this.client.refresh( new TokenSet({ access_token: 'present', refresh_token: 'refreshValue', - id_token: jose2.JWT.sign( - { - sub: 'foo', - }, - this.client.client_secret, - { - issuer: this.client.issuer.issuer, - audience: this.client.client_id, - expiresIn: '6m', - }, - ), + id_token: await new jose.SignJWT({ + sub: 'foo', + iss: this.client.issuer.issuer, + aud: this.client.client_id, + }) + .setIssuedAt() + .setProtectedHeader({ alg: 'HS256' }) + .setExpirationTime('6m') + .sign(new TextEncoder().encode(this.client.client_secret)), }), ); }); - it('rejects when returned ID Token sub does not match the one passed in', function () { + it('rejects when returned ID Token sub does not match the one passed in', async function () { nock('https://op.example.com') .matchHeader('Accept', 'application/json') .post('/token') // to make sure filteringRequestBody works .reply(200, { access_token: 'present', refresh_token: 'refreshValue', - id_token: jose2.JWT.sign( - { - sub: 'bar', - }, - this.client.client_secret, - { - issuer: this.client.issuer.issuer, - audience: this.client.client_id, - expiresIn: '5m', - }, - ), + id_token: await new jose.SignJWT({ + sub: 'bar', + iss: this.client.issuer.issuer, + aud: this.client.client_id, + }) + .setProtectedHeader({ alg: 'HS256' }) + .setIssuedAt() + .setExpirationTime('5m') + .sign(new TextEncoder().encode(this.client.client_secret)), }); return this.client @@ -1443,17 +1423,15 @@ describe('Client', () => { new TokenSet({ access_token: 'present', refresh_token: 'refreshValue', - id_token: jose2.JWT.sign( - { - sub: 'foo', - }, - this.client.client_secret, - { - issuer: this.client.issuer.issuer, - audience: this.client.client_id, - expiresIn: '5m', - }, - ), + id_token: await new jose.SignJWT({ + sub: 'foo', + iss: this.client.issuer.issuer, + aud: this.client.client_id, + }) + .setProtectedHeader({ alg: 'HS256' }) + .setExpirationTime('5m') + .setIssuedAt() + .sign(new TextEncoder().encode(this.client.client_secret)), }), ) .then(fail, (error) => { @@ -2404,7 +2382,7 @@ describe('Client', () => { token_endpoint_auth_signing_alg_values_supported: ['ES256', 'ES384'], }); - const keystore = new jose2.JWKS.KeyStore(); + const keystore = new KeyStore(); return keystore.generate('EC', 'P-256').then(() => { const client = new issuer.Client( @@ -2505,7 +2483,7 @@ describe('Client', () => { token_endpoint: 'https://op.example.com/token', }); - const keystore = new jose2.JWKS.KeyStore(); + const keystore = new KeyStore(); return keystore.generate('EC', 'P-256').then(() => { const client = new issuer.Client( @@ -2537,7 +2515,7 @@ describe('Client', () => { }); before(function () { - this.keystore = new jose2.JWKS.KeyStore(); + this.keystore = new KeyStore(); return this.keystore.generate('RSA'); }); @@ -2572,12 +2550,20 @@ describe('Client', () => { token_endpoint_auth_method: 'tls_client_auth', }); - this.IdToken = async (key, alg, payload) => { - return jose2.JWS.sign(payload, key, { - alg, - typ: 'oauth-authz-req+jwt', - kid: alg.startsWith('HS') ? undefined : key.kid, - }); + this.IdToken = async (jwkOrSecret, alg, payload) => { + let key; + if (jwkOrSecret instanceof Uint8Array) { + key = jwkOrSecret; + } else { + key = await jose.importJWK(jwkOrSecret, alg); + } + return new jose.SignJWT(payload) + .setProtectedHeader({ + alg, + typ: 'oauth-authz-req+jwt', + kid: alg.startsWith('HS') ? undefined : key.kid, + }) + .sign(key); }; }); @@ -3537,7 +3523,7 @@ describe('Client', () => { describe('signed and encrypted responses', function () { before(function () { - this.keystore = jose2.JWKS.asKeyStore({ + this.keystore = new KeyStore({ keys: [ { kty: 'EC', @@ -3859,7 +3845,7 @@ describe('Client', () => { describe('#requestObject', function () { before(function () { - this.keystore = new jose2.JWKS.KeyStore(); + this.keystore = new KeyStore(); return this.keystore.generate('RSA'); }); @@ -3894,7 +3880,7 @@ describe('Client', () => { }); it('verifies keystore has the appropriate key', async function () { - const keystore = new jose2.JWKS.KeyStore(); + const keystore = new KeyStore(); await keystore.generate('EC'); const client = new this.issuer.Client( { client_id: 'identifier', request_object_signing_alg: 'EdDSA' }, @@ -4165,7 +4151,7 @@ describe('Client', () => { describe('#requestObject (encryption when multiple keys match)', function () { before(function () { - this.keystore = new jose2.JWKS.KeyStore(); + this.keystore = new KeyStore(); return Promise.all([this.keystore.generate('RSA'), this.keystore.generate('RSA')]); }); diff --git a/test/client/dpop.test.js b/test/client/dpop.test.js index 04aa4ef8..4b1311bc 100644 --- a/test/client/dpop.test.js +++ b/test/client/dpop.test.js @@ -2,7 +2,7 @@ const { isUndefined } = require('util'); const { expect } = require('chai'); const nock = require('nock'); -const jose2 = require('jose2'); +const jose = require('jose'); const { Issuer, @@ -20,8 +20,6 @@ const issuer = new Issuer({ dpop_signing_alg_values_supported: ['PS512', 'PS384'], }); -const privateKey = jose2.JWK.generateSync('EC').keyObject; - const fail = () => { throw new Error('expected promise to be rejected'); }; @@ -49,42 +47,58 @@ describe('DPoP', () => { }); }); - it('DPoP Private Key can be passed also as valid createPrivateKey input', async function () { - if (parseInt(process.versions.node, 10) >= 16) { - const jwk = (await jose2.JWK.generate('EC')).toJWK(true); - await this.client.dpopProof({}, { format: 'jwk', key: jwk }); - } + if (jose.cryptoRuntime === 'node:crypto') { + it('DPoP Private Key can be passed also as valid createPrivateKey input', async function () { + if (parseInt(process.versions.node, 10) >= 16) { + const jwk = await jose.exportJWK( + ( + await jose.generateKeyPair('ES256', { extractable: true }) + ).privateKey, + ); + await this.client.dpopProof({}, { format: 'jwk', key: jwk }); + } - { - const pem = (await jose2.JWK.generate('EC')).toPEM(true); - await this.client.dpopProof({}, pem); - await this.client.dpopProof({}, { key: pem, format: 'pem' }); - } + { + const pem = await jose.exportPKCS8( + ( + await jose.generateKeyPair('ES256', { extractable: true }) + ).privateKey, + ); + await this.client.dpopProof({}, pem); + await this.client.dpopProof({}, { key: pem, format: 'pem' }); + } - { - const der = (await jose2.JWK.generate('EC')).keyObject.export({ - format: 'der', - type: 'pkcs8', - }); - await this.client.dpopProof({}, { key: der, format: 'der', type: 'pkcs8' }); - } + { + const der = ( + await jose.generateKeyPair('ES256', { extractable: true }) + ).privateKey.export({ + format: 'der', + type: 'pkcs8', + }); + await this.client.dpopProof({}, { key: der, format: 'der', type: 'pkcs8' }); + } - { - const der = (await jose2.JWK.generate('EC')).keyObject.export({ - format: 'der', - type: 'sec1', - }); - await this.client.dpopProof({}, { key: der, format: 'der', type: 'sec1' }); - } + { + const der = ( + await jose.generateKeyPair('ES256', { extractable: true }) + ).privateKey.export({ + format: 'der', + type: 'sec1', + }); + await this.client.dpopProof({}, { key: der, format: 'der', type: 'sec1' }); + } - { - const der = (await jose2.JWK.generate('RSA')).keyObject.export({ - format: 'der', - type: 'pkcs1', - }); - await this.client.dpopProof({}, { key: der, format: 'der', type: 'pkcs1' }); - } - }); + { + const der = ( + await jose.generateKeyPair('RS256', { extractable: true }) + ).privateKey.export({ + format: 'der', + type: 'pkcs1', + }); + await this.client.dpopProof({}, { key: der, format: 'der', type: 'pkcs1' }); + } + }); + } it('DPoP Proof JWT w/o ath', async function () { const proof = await this.client.dpopProof( @@ -94,151 +108,140 @@ describe('DPoP', () => { baz: true, }, ( - await jose2.JWK.generate('RSA') - ).keyObject, + await jose.generateKeyPair('RS256', { extractable: true }) + ).privateKey, ); - const decoded = jose2.JWT.decode(proof, { complete: true }); - expect(decoded).to.have.nested.property('header.typ', 'dpop+jwt'); - expect(decoded).to.have.nested.property('payload.iat'); - expect(decoded).to.have.nested.property('payload.jti'); - expect(decoded).to.have.nested.property('payload.htu', 'foo'); - expect(decoded).to.have.nested.property('payload.htm', 'bar'); - expect(decoded).to.have.nested.property('payload.baz', true); - expect(decoded).to.have.nested.property('header.jwk').that.has.keys('kty', 'e', 'n'); + const header = jose.decodeProtectedHeader(proof); + const payload = jose.decodeJwt(proof); + expect(header).to.have.property('jwk').that.has.keys('kty', 'e', 'n'); + expect(header).to.have.property('typ', 'dpop+jwt'); + expect(payload).to.have.property('iat'); + expect(payload).to.have.property('jti'); + expect(payload).to.have.property('htu', 'foo'); + expect(payload).to.have.property('htm', 'bar'); + expect(payload).to.have.property('baz', true); expect( - jose2.JWT.decode( - await this.client.dpopProof({}, (await jose2.JWK.generate('EC')).keyObject), + jose.decodeProtectedHeader( + await this.client.dpopProof( + {}, + ( + await jose.generateKeyPair('ES256', { extractable: true }) + ).privateKey, + ), { complete: true }, ), ) - .to.have.nested.property('header.jwk') + .to.have.property('jwk') .that.has.keys('kty', 'crv', 'x', 'y'); expect( - jose2.JWT.decode( - await this.client.dpopProof({}, (await jose2.JWK.generate('OKP')).keyObject), - { complete: true }, + jose.decodeProtectedHeader( + await this.client.dpopProof( + {}, + ( + await jose.generateKeyPair('EdDSA', { extractable: true }) + ).privateKey, + ), ), ) - .to.have.nested.property('header.jwk') + .to.have.property('jwk') .that.has.keys('kty', 'crv', 'x'); }); it('DPoP Proof JWT w/ ath', async function () { + const { privateKey } = await jose.generateKeyPair('ES256', { extractable: true }); const proof = await this.client.dpopProof( { htu: 'foo', htm: 'bar', }, - ( - await jose2.JWK.generate('EC') - ).keyObject, + privateKey, 'foo', ); - const decoded = jose2.JWT.decode(proof, { complete: true }); - expect(decoded).to.have.nested.property( - 'payload.ath', - 'LCa0a2j_xo_5m0U8HTBBNBNCLXBkg7-g-YpeiGJm564', - ); + const payload = jose.decodeJwt(proof); + expect(payload).to.have.property('ath', 'LCa0a2j_xo_5m0U8HTBBNBNCLXBkg7-g-YpeiGJm564'); }); - it('else this.issuer.dpop_signing_alg_values_supported is used', async function () { - const proof = await this.client.dpopProof( - {}, - ( - await jose2.JWK.generate('RSA', 2048) - ).keyObject, - ); - // 256 is not supported by the issuer, next one in line is PS384 - expect(jose2.JWT.decode(proof, { complete: true })).to.have.nested.property( - 'header.alg', - 'PS384', - ); - }); + if (jose.cryptoRuntime === 'node:crypto') { + it('else this.issuer.dpop_signing_alg_values_supported is used', async function () { + const proof = await this.client.dpopProof( + {}, + ( + await jose.generateKeyPair('RS256', { extractable: true }) + ).privateKey, + ); + // 256 is not supported by the issuer, next one in line is PS384 + expect(jose.decodeProtectedHeader(proof)).to.have.property('alg', 'PS384'); + }); + } it('unless the key dictates an algorithm', async function () { { const proof = await this.client.dpopProof( {}, ( - await jose2.JWK.generate('OKP', 'Ed25519') - ).keyObject, - ); - expect(jose2.JWT.decode(proof, { complete: true })).to.have.nested.property( - 'header.alg', - 'EdDSA', + await jose.generateKeyPair('EdDSA', { extractable: true }) + ).privateKey, ); + expect(jose.decodeProtectedHeader(proof)).to.have.property('alg', 'EdDSA'); } - if (!('electron' in process.versions)) { + if (!('electron' in process.versions) && jose.cryptoRuntime === 'node:crypto') { const proof = await this.client.dpopProof( {}, ( - await jose2.JWK.generate('OKP', 'Ed448') - ).keyObject, - ); - expect(jose2.JWT.decode(proof, { complete: true })).to.have.nested.property( - 'header.alg', - 'EdDSA', + await jose.generateKeyPair('EdDSA', { crv: 'Ed448' }) + ).privateKey, ); + expect(jose.decodeProtectedHeader(proof)).to.have.property('alg', 'EdDSA'); } { const proof = await this.client.dpopProof( {}, ( - await jose2.JWK.generate('EC', 'P-256') - ).keyObject, - ); - expect(jose2.JWT.decode(proof, { complete: true })).to.have.nested.property( - 'header.alg', - 'ES256', + await jose.generateKeyPair('ES256', { extractable: true }) + ).privateKey, ); + expect(jose.decodeProtectedHeader(proof)).to.have.property('alg', 'ES256'); } - if (!('electron' in process.versions)) { + if (!('electron' in process.versions) && jose.cryptoRuntime === 'node:crypto') { const proof = await this.client.dpopProof( {}, ( - await jose2.JWK.generate('EC', 'secp256k1') - ).keyObject, - ); - expect(jose2.JWT.decode(proof, { complete: true })).to.have.nested.property( - 'header.alg', - 'ES256K', + await jose.generateKeyPair('ES256K', { extractable: true }) + ).privateKey, ); + expect(jose.decodeProtectedHeader(proof)).to.have.property('alg', 'ES256K'); } { const proof = await this.client.dpopProof( {}, ( - await jose2.JWK.generate('EC', 'P-384') - ).keyObject, - ); - expect(jose2.JWT.decode(proof, { complete: true })).to.have.nested.property( - 'header.alg', - 'ES384', + await jose.generateKeyPair('ES384', { extractable: true }) + ).privateKey, ); + expect(jose.decodeProtectedHeader(proof)).to.have.property('alg', 'ES384'); } { const proof = await this.client.dpopProof( {}, ( - await jose2.JWK.generate('EC', 'P-521') - ).keyObject, - ); - expect(jose2.JWT.decode(proof, { complete: true })).to.have.nested.property( - 'header.alg', - 'ES512', + await jose.generateKeyPair('ES512', { extractable: true }) + ).privateKey, ); + expect(jose.decodeProtectedHeader(proof)).to.have.property('alg', 'ES512'); } }); }); it('is enabled for userinfo', async function () { + const { privateKey } = await jose.generateKeyPair('ES256', { extractable: true }); + nock('https://op.example.com').get('/me').reply(200, { sub: 'foo' }); await this.client.userinfo('foo', { DPoP: privateKey }); @@ -246,15 +249,17 @@ describe('DPoP', () => { expect(this.httpOpts).to.have.nested.property('headers.DPoP'); const proof = this.httpOpts.headers.DPoP; - const proofJWT = jose2.JWT.decode(proof, { complete: true }); - expect(proofJWT).to.have.nested.property('payload.ath'); + const proofJWT = jose.decodeJwt(proof); + expect(proofJWT).to.have.property('ath'); }); it('handles DPoP nonce in userinfo', async function () { + const { privateKey } = await jose.generateKeyPair('ES256', { extractable: true }); + nock('https://op.example.com') .get('/me') .matchHeader('DPoP', (proof) => { - const { nonce } = jose2.JWT.decode(proof); + const { nonce } = jose.decodeJwt(proof); expect(nonce).to.be.undefined; return true; }) @@ -264,21 +269,21 @@ describe('DPoP', () => { }) .get('/me') .matchHeader('DPoP', (proof) => { - const { nonce } = jose2.JWT.decode(proof); + const { nonce } = jose.decodeJwt(proof); expect(nonce).to.eq('eyJ7S_zG.eyJH0-Z.HX4w-7v'); return true; }) .reply(200, { sub: 'foo' }) .get('/me') .matchHeader('DPoP', (proof) => { - const { nonce } = jose2.JWT.decode(proof); + const { nonce } = jose.decodeJwt(proof); expect(nonce).to.eq('eyJ7S_zG.eyJH0-Z.HX4w-7v'); return true; }) .reply(200, { sub: 'foo' }) .get('/me') .matchHeader('DPoP', (proof) => { - const { nonce } = jose2.JWT.decode(proof); + const { nonce } = jose.decodeJwt(proof); expect(nonce).to.eq('eyJ7S_zG.eyJH0-Z.HX4w-7v'); return true; }) @@ -295,10 +300,12 @@ describe('DPoP', () => { }); it('handles DPoP nonce in grant', async function () { + const { privateKey } = await jose.generateKeyPair('ES256', { extractable: true }); + nock('https://op.example.com') .post('/token') .matchHeader('DPoP', (proof) => { - const { nonce } = jose2.JWT.decode(proof); + const { nonce } = jose.decodeJwt(proof); expect(nonce).to.be.undefined; return true; }) @@ -311,21 +318,21 @@ describe('DPoP', () => { ) .post('/token') .matchHeader('DPoP', (proof) => { - const { nonce } = jose2.JWT.decode(proof); + const { nonce } = jose.decodeJwt(proof); expect(nonce).to.eq('eyJ7S_zG.eyJH0-Z.HX4w-7v'); return true; }) .reply(200, { access_token: 'foo' }) .post('/token') .matchHeader('DPoP', (proof) => { - const { nonce } = jose2.JWT.decode(proof); + const { nonce } = jose.decodeJwt(proof); expect(nonce).to.eq('eyJ7S_zG.eyJH0-Z.HX4w-7v'); return true; }) .reply(200, { access_token: 'foo' }) .post('/token') .matchHeader('DPoP', (proof) => { - const { nonce } = jose2.JWT.decode(proof); + const { nonce } = jose.decodeJwt(proof); expect(nonce).to.eq('eyJ7S_zG.eyJH0-Z.HX4w-7v'); return true; }) @@ -342,10 +349,11 @@ describe('DPoP', () => { }); it('handles DPoP nonce in requestResource', async function () { + const { privateKey } = await jose.generateKeyPair('ES256', { extractable: true }); nock('https://rs.example.com') .get('/resource') .matchHeader('DPoP', (proof) => { - const { nonce } = jose2.JWT.decode(proof); + const { nonce } = jose.decodeJwt(proof); expect(nonce).to.be.undefined; return true; }) @@ -355,21 +363,21 @@ describe('DPoP', () => { }) .get('/resource') .matchHeader('DPoP', (proof) => { - const { nonce } = jose2.JWT.decode(proof); + const { nonce } = jose.decodeJwt(proof); expect(nonce).to.eq('eyJ7S_zG.eyJH0-Z.HX4w-7v'); return true; }) .reply(200, { sub: 'foo' }) .get('/resource') .matchHeader('DPoP', (proof) => { - const { nonce } = jose2.JWT.decode(proof); + const { nonce } = jose.decodeJwt(proof); expect(nonce).to.eq('eyJ7S_zG.eyJH0-Z.HX4w-7v'); return true; }) .reply(200, { sub: 'foo' }) .get('/resource') .matchHeader('DPoP', (proof) => { - const { nonce } = jose2.JWT.decode(proof); + const { nonce } = jose.decodeJwt(proof); expect(nonce).to.eq('eyJ7S_zG.eyJH0-Z.HX4w-7v'); return true; }) @@ -393,6 +401,7 @@ describe('DPoP', () => { }); it('is enabled for requestResource', async function () { + const { privateKey } = await jose.generateKeyPair('ES256', { extractable: true }); nock('https://rs.example.com') .matchHeader('Transfer-Encoding', isUndefined) .matchHeader('Content-Length', isUndefined) @@ -407,11 +416,12 @@ describe('DPoP', () => { expect(this.httpOpts).to.have.nested.property('headers.DPoP'); const proof = this.httpOpts.headers.DPoP; - const proofJWT = jose2.JWT.decode(proof, { complete: true }); - expect(proofJWT).to.have.nested.property('payload.ath'); + const proofJWT = jose.decodeJwt(proof); + expect(proofJWT).to.have.property('ath'); }); it('is enabled for grant', async function () { + const { privateKey } = await jose.generateKeyPair('ES256', { extractable: true }); nock('https://op.example.com').post('/token').reply(200, { access_token: 'foo' }); await this.client.grant({ grant_type: 'foo' }, { DPoP: privateKey }); @@ -420,6 +430,7 @@ describe('DPoP', () => { }); it('is enabled for refresh', async function () { + const { privateKey } = await jose.generateKeyPair('ES256', { extractable: true }); nock('https://op.example.com').post('/token').reply(200, { access_token: 'foo' }); await this.client.refresh('foo', { DPoP: privateKey }); @@ -428,6 +439,7 @@ describe('DPoP', () => { }); it('is enabled for oauthCallback', async function () { + const { privateKey } = await jose.generateKeyPair('ES256', { extractable: true }); nock('https://op.example.com').post('/token').reply(200, { access_token: 'foo' }); await this.client.oauthCallback('foo', { code: 'foo' }, {}, { DPoP: privateKey }); @@ -436,6 +448,7 @@ describe('DPoP', () => { }); it('is enabled for callback', async function () { + const { privateKey } = await jose.generateKeyPair('ES256', { extractable: true }); nock('https://op.example.com').post('/token').reply(200, { access_token: 'foo' }); try { @@ -446,6 +459,7 @@ describe('DPoP', () => { }); it('is enabled for deviceAuthorization', async function () { + const { privateKey } = await jose.generateKeyPair('ES256', { extractable: true }); nock('https://op.example.com').post('/device').reply(200, { expires_in: 60, device_code: 'foo', diff --git a/test/client/implicit_kid.test.js b/test/client/implicit_kid.test.js index 728f3b9e..445b9e16 100644 --- a/test/client/implicit_kid.test.js +++ b/test/client/implicit_kid.test.js @@ -1,12 +1,12 @@ const { expect } = require('chai'); -const jose2 = require('jose2'); const nock = require('nock'); const Issuer = require('../../lib/issuer'); const clientInternal = require('../../lib/helpers/client'); +const KeyStore = require('../keystore'); async function noKidJWKS() { - const store = new jose2.JWKS.KeyStore(); + const store = new KeyStore(); await store.generate('EC'); const jwks = store.toJWKS(true); delete jwks.keys[0].kid; diff --git a/test/client/mtls.test.js b/test/client/mtls.test.js index 4177daf9..59c55a8b 100644 --- a/test/client/mtls.test.js +++ b/test/client/mtls.test.js @@ -3,7 +3,7 @@ const path = require('path'); const { expect } = require('chai'); const nock = require('nock'); -const jose2 = require('jose2'); +const jose = require('jose'); const { Issuer, custom } = require('../../lib'); const clientHelpers = require('../../lib/helpers/client'); @@ -116,17 +116,17 @@ describe('mutual-TLS', () => { let { form: { client_assertion: jwt }, } = await clientHelpers.authFor.call(this.jwtAuthClient, 'token'); - let { aud } = jose2.JWT.decode(jwt); + let { aud } = jose.decodeJwt(jwt); expect(aud).to.deep.equal(['https://op.example.com', 'https://op.example.com/token']); ({ form: { client_assertion: jwt }, } = await clientHelpers.authFor.call(this.jwtAuthClient, 'introspection')); - ({ aud } = jose2.JWT.decode(jwt)); + ({ aud } = jose.decodeJwt(jwt)); expect(aud).to.deep.equal(['https://op.example.com', 'https://op.example.com/token']); ({ form: { client_assertion: jwt }, } = await clientHelpers.authFor.call(this.jwtAuthClient, 'revocation')); - ({ aud } = jose2.JWT.decode(jwt)); + ({ aud } = jose.decodeJwt(jwt)); expect(aud).to.deep.equal(['https://op.example.com', 'https://op.example.com/token']); }); diff --git a/test/client/register_client.test.js b/test/client/register_client.test.js index f372c1c7..0d5f2386 100644 --- a/test/client/register_client.test.js +++ b/test/client/register_client.test.js @@ -1,11 +1,11 @@ const { isNumber, isUndefined } = require('util'); const { expect } = require('chai'); -const jose2 = require('jose2'); const sinon = require('sinon'); const nock = require('nock'); const { Issuer, custom } = require('../../lib'); +const KeyStore = require('../keystore'); const fail = () => { throw new Error('expected promise to be rejected'); @@ -89,7 +89,7 @@ describe('Client#register', () => { describe('with keystore (as option)', function () { it('enriches the registration with jwks if not provided (or jwks_uri)', function () { - const keystore = new jose2.JWKS.KeyStore(); + const keystore = new KeyStore(); nock('https://op.example.com') .filteringRequestBody(function (body) { @@ -109,7 +109,7 @@ describe('Client#register', () => { }); it('ignores the keystore during registration if jwks is provided', function () { - const keystore = new jose2.JWKS.KeyStore(); + const keystore = new KeyStore(); nock('https://op.example.com') .filteringRequestBody(function (body) { @@ -134,7 +134,7 @@ describe('Client#register', () => { }); it('ignores the keystore during registration if jwks_uri is provided', function () { - const keystore = new jose2.JWKS.KeyStore(); + const keystore = new KeyStore(); nock('https://op.example.com') .filteringRequestBody(function (body) { @@ -166,7 +166,7 @@ describe('Client#register', () => { }); it('does not accept oct keys', function () { - const keystore = new jose2.JWKS.KeyStore(); + const keystore = new KeyStore(); return keystore.generate('oct', 32).then(() => { return issuer.Client.register({}, { jwks: keystore.toJWKS(true) }).then( diff --git a/test/client/self_issued.test.js b/test/client/self_issued.test.js index cda850ce..f43ed4da 100644 --- a/test/client/self_issued.test.js +++ b/test/client/self_issued.test.js @@ -1,7 +1,7 @@ const { expect } = require('chai'); const nock = require('nock'); const timekeeper = require('timekeeper'); -const jose2 = require('jose2'); +const jose = require('jose'); const { Issuer } = require('../../lib'); @@ -35,29 +35,33 @@ describe('Validating Self-Issued OP responses', () => { Object.assign(this, { issuer, client }); }); - const idToken = (claims = {}) => { - const jwk = jose2.JWK.generateSync('EC'); - return jose2.JWT.sign( - { - sub_jwk: jwk.toJWK(), - sub: jwk.thumbprint, - ...claims, - }, - jwk, - { expiresIn: '2h', issuer: 'https://self-issued.me', audience: 'https://rp.example.com/cb' }, - ); - }; + async function idToken(claims = {}) { + const kp = await jose.generateKeyPair('ES256', { extractable: true }); + const jwk = await jose.exportJWK(kp.publicKey); + const sub = await jose.calculateJwkThumbprint(jwk); + return await new jose.SignJWT({ + sub_jwk: jwk, + sub, + ...claims, + }) + .setIssuedAt() + .setProtectedHeader({ alg: 'ES256' }) + .setIssuer('https://self-issued.me') + .setAudience('https://rp.example.com/cb') + .setExpirationTime('2h') + .sign(kp.privateKey); + } describe('consuming an ID Token response', () => { - it('consumes a self-issued response', function () { + it('consumes a self-issued response', async function () { const { client } = this; - return client.callback(undefined, { id_token: idToken() }); + return client.callback(undefined, { id_token: await idToken() }); }); - it('expects sub_jwk to be in the ID Token claims', function () { + it('expects sub_jwk to be in the ID Token claims', async function () { const { client } = this; return client - .callback(undefined, { id_token: idToken({ sub_jwk: undefined }) }) + .callback(undefined, { id_token: await idToken({ sub_jwk: undefined }) }) .then(fail, (err) => { expect(err.name).to.equal('RPError'); expect(err.message).to.equal('missing required JWT property sub_jwk'); @@ -65,10 +69,10 @@ describe('Validating Self-Issued OP responses', () => { }); }); - it('expects sub_jwk to be a public JWK', function () { + it('expects sub_jwk to be a public JWK', async function () { const { client } = this; return client - .callback(undefined, { id_token: idToken({ sub_jwk: 'foobar' }) }) + .callback(undefined, { id_token: await idToken({ sub_jwk: 'foobar' }) }) .then(fail, (err) => { expect(err.name).to.equal('RPError'); expect(err.message).to.equal('failed to use sub_jwk claim as an asymmetric JSON Web Key'); @@ -76,13 +80,15 @@ describe('Validating Self-Issued OP responses', () => { }); }); - it('expects sub to be the thumbprint of the sub_jwk', function () { + it('expects sub to be the thumbprint of the sub_jwk', async function () { const { client } = this; - return client.callback(undefined, { id_token: idToken({ sub: 'foo' }) }).then(fail, (err) => { - expect(err.name).to.equal('RPError'); - expect(err.message).to.equal('failed to match the subject with sub_jwk'); - expect(err).to.have.property('jwt'); - }); + return client + .callback(undefined, { id_token: await idToken({ sub: 'foo' }) }) + .then(fail, (err) => { + expect(err.name).to.equal('RPError'); + expect(err.message).to.equal('failed to match the subject with sub_jwk'); + expect(err).to.have.property('jwt'); + }); }); }); }); diff --git a/test/issuer/issuer_instance.test.js b/test/issuer/issuer_instance.test.js index 2f1fff14..f58bfcb4 100644 --- a/test/issuer/issuer_instance.test.js +++ b/test/issuer/issuer_instance.test.js @@ -2,10 +2,10 @@ const { expect } = require('chai'); const LRU = require('lru-cache'); const nock = require('nock'); const sinon = require('sinon'); -const jose2 = require('jose2'); const { Issuer, custom } = require('../../lib'); const issuerInternal = require('../../lib/helpers/issuer'); +const KeyStore = require('../keystore'); const fail = () => { throw new Error('expected promise to be rejected'); @@ -22,7 +22,7 @@ describe('Issuer', () => { }); before(function () { - this.keystore = new jose2.JWKS.KeyStore(); + this.keystore = new KeyStore(); return this.keystore.generate('RSA'); }); @@ -115,7 +115,11 @@ describe('Issuer', () => { return this.keystore.generate('RSA', undefined, { kid }).then(() => { nock('https://op.example.com').get('/certs').reply(200, this.keystore.toJWKS()); - return issuerInternal.queryKeyStore.call(this.issuer, { alg: 'RS256', kid, use: 'sig' }); + return issuerInternal.queryKeyStore + .call(this.issuer, { alg: 'RS256', kid, use: 'sig' }) + .then((result) => { + expect(result).to.have.lengthOf(2); + }); }); }); diff --git a/test/keystore.js b/test/keystore.js new file mode 100644 index 00000000..40007ead --- /dev/null +++ b/test/keystore.js @@ -0,0 +1,97 @@ +const jose = require('jose'); +const crypto = require('crypto'); +const base64url = require('../lib/helpers/base64url'); + +module.exports = class KeyStore { + constructor({ keys } = {}) { + this.keys = keys || []; + } + + async generate(kty, crvOrSize, { alg, kid, use } = {}) { + let kp; + if (kty !== 'oct' && alg) { + kp = await jose.generateKeyPair(alg); + } else { + switch (kty) { + case 'EC': { + switch (crvOrSize) { + case undefined: + case 'P-256': + kp = await jose.generateKeyPair('ES256', { extractable: true }); + break; + case 'P-384': + kp = await jose.generateKeyPair('ES384', { extractable: true }); + break; + case 'P-521': + kp = await jose.generateKeyPair('ES512', { extractable: true }); + break; + case 'secp256k1': + kp = await jose.generateKeyPair('ES256K', { extractable: true }); + break; + } + break; + } + case 'oct': { + const secret = crypto.randomBytes((crvOrSize || 256) >> 3); + const jwk = { + kty: 'oct', + use: use, + alg, + k: base64url.encode(secret), + }; + jwk.kid = kid || (await jose.calculateJwkThumbprint(jwk)); + this.keys.push(jwk); + return; + } + case 'RSA': { + kp = await jose.generateKeyPair('RS256', { modulusLength: crvOrSize, extractable: true }); + break; + } + case 'OKP': { + switch (crvOrSize) { + case undefined: + case 'Ed25519': + case 'Ed448': + kp = await jose.generateKeyPair('EdDSA', { crv: crvOrSize, extractable: true }); + break; + case 'X25519': + case 'X448': + kp = await jose.generateKeyPair('ECDH-ES', { crv: crvOrSize, extractable: true }); + break; + } + break; + } + } + } + const jwk = { + ...(await jose.exportJWK(kp.privateKey)), + kid, + }; + jwk.kid || (jwk.kid = await jose.calculateJwkThumbprint(jwk)); + if (use) jwk.use = use; + this.keys.push(jwk); + } + + get(query) { + if (!query) { + return this.keys[0]; + } + const { kty } = query || {}; + return this.keys.find((jwk) => { + return jwk.kty === kty; + }); + } + + toJWKS(includePrivate) { + if (includePrivate) { + return { keys: this.keys }; + } + + return { + keys: this.keys.map((privateKey) => { + const { k, d, dp, dq, p, q, qi, ...jwk } = privateKey; + return jwk; + }), + }; + } +}; diff --git a/test/passport/passport_strategy.test.js b/test/passport/passport_strategy.test.js index 3ac1e1e2..71e715c1 100644 --- a/test/passport/passport_strategy.test.js +++ b/test/passport/passport_strategy.test.js @@ -104,7 +104,7 @@ describe('OpenIDConnectStrategy', () => { }); }); - describe('initate', function () { + describe('initiate', function () { it('starts authentication requests for GETs', function () { const params = { foo: 'bar' }; const strategy = new Strategy({ client: this.client, params }, () => {});