From 133a022cce8e0d7a386b59163c18c100c80df2ab Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Fri, 8 Sep 2023 08:40:54 +0200 Subject: [PATCH] feat(DPoP): remove experimental warning, DPoP is now RFC9449 --- README.md | 4 +- lib/client.js | 137 ++++++++++++++++++++++---------------------------- 2 files changed, 62 insertions(+), 79 deletions(-) diff --git a/README.md b/README.md index c7cbd5b8..f4d4bbe6 100644 --- a/README.md +++ b/README.md @@ -42,10 +42,10 @@ openid-client. - self_signed_tls_client_auth - [RFC9101 - OAuth 2.0 JWT-Secured Authorization Request (JAR)][feature-jar] - [RFC9126 - OAuth 2.0 Pushed Authorization Requests (PAR)][feature-par] +- [RFC9449 - OAuth 2.0 Demonstration of Proof-of-Possession at the Application Layer (DPoP)][feature-dpop] - [OpenID Connect RP-Initiated Logout 1.0][feature-rp-logout] - [Financial-grade API Security Profile 1.0 - Part 2: Advanced (FAPI)][feature-fapi] - [JWT Secured Authorization Response Mode for OAuth 2.0 (JARM)][feature-jarm] -- [OAuth 2.0 Demonstration of Proof-of-Possession at the Application Layer (DPoP) - draft 04][feature-dpop] - [OAuth 2.0 Authorization Server Issuer Identification][feature-iss] Updates to draft specifications are released as MINOR library versions, @@ -282,7 +282,7 @@ See [Customizing (docs)][documentation-customizing]. [feature-rp-logout]: https://openid.net/specs/openid-connect-rpinitiated-1_0.html [feature-jarm]: https://openid.net/specs/oauth-v2-jarm.html [feature-fapi]: https://openid.net/specs/openid-financial-api-part-2-1_0.html -[feature-dpop]: https://tools.ietf.org/html/draft-ietf-oauth-dpop-04 +[feature-dpop]: https://www.rfc-editor.org/rfc/rfc9449.html [feature-par]: https://www.rfc-editor.org/rfc/rfc9126.html [feature-jar]: https://www.rfc-editor.org/rfc/rfc9101.html [feature-iss]: https://www.rfc-editor.org/rfc/rfc9207.html diff --git a/lib/client.js b/lib/client.js index 6ec4b51c..069f9b56 100644 --- a/lib/client.js +++ b/lib/client.js @@ -1612,6 +1612,66 @@ class BaseClient { const { payload } = await this.validateJWT(response, expectedAlg, ['iss', 'exp', 'aud']); return pickCb(payload); } + + /** + * @name dpopProof + * @api private + */ + async dpopProof(payload, privateKeyInput, accessToken) { + if (!isPlainObject(payload)) { + throw new TypeError('payload must be a plain object'); + } + + let privateKey; + if (isKeyObject(privateKeyInput)) { + privateKey = privateKeyInput; + } else { + privateKey = crypto.createPrivateKey(privateKeyInput); + } + + 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'); + } + + if (!alg) { + throw new TypeError('could not determine DPoP JWS Algorithm'); + } + + return new jose.SignJWT({ + ath: accessToken + ? base64url.encode(crypto.createHash('sha256').update(accessToken).digest()) + : undefined, + ...payload, + }) + .setProtectedHeader({ + alg, + typ: 'dpop+jwt', + jwk: await getJwk(privateKey, privateKeyInput), + }) + .setIssuedAt() + .setJti(random()) + .sign(privateKey); + } } const RSPS = /^(?:RS|PS)(?:256|384|512)$/; @@ -1706,83 +1766,6 @@ async function getJwk(privateKey, privateKeyInput) { return jwk; } -/** - * @name dpopProof - * @api private - */ -async function dpopProof(payload, privateKeyInput, accessToken) { - if (!isPlainObject(payload)) { - throw new TypeError('payload must be a plain object'); - } - - let privateKey; - if (isKeyObject(privateKeyInput)) { - privateKey = privateKeyInput; - } else { - privateKey = crypto.createPrivateKey(privateKeyInput); - } - - 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'); - } - - if (!alg) { - throw new TypeError('could not determine DPoP JWS Algorithm'); - } - - return new jose.SignJWT({ - ath: accessToken - ? base64url.encode(crypto.createHash('sha256').update(accessToken).digest()) - : undefined, - ...payload, - }) - .setProtectedHeader({ - alg, - typ: 'dpop+jwt', - jwk: await getJwk(privateKey, privateKeyInput), - }) - .setIssuedAt() - .setJti(random()) - .sign(privateKey); -} - -Object.defineProperty(BaseClient.prototype, 'dpopProof', { - enumerable: true, - configurable: true, - value(...args) { - process.emitWarning( - 'The DPoP APIs implements an IETF draft (https://www.ietf.org/archive/id/draft-ietf-oauth-dpop-04.html). Breaking draft implementations are included as minor versions of the openid-client library, therefore, the ~ semver operator should be used and close attention be payed to library changelog as well as the drafts themselves.', - 'DraftWarning', - ); - Object.defineProperty(BaseClient.prototype, 'dpopProof', { - enumerable: true, - configurable: true, - value: dpopProof, - }); - return this.dpopProof(...args); - }, -}); - module.exports = (issuer, aadIssValidation = false) => class Client extends BaseClient { constructor(...args) {