Skip to content

Commit

Permalink
feat(DPoP): remove experimental warning, DPoP is now RFC9449
Browse files Browse the repository at this point in the history
  • Loading branch information
panva committed Sep 8, 2023
1 parent 7e045ca commit 133a022
Show file tree
Hide file tree
Showing 2 changed files with 62 additions and 79 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
137 changes: 60 additions & 77 deletions lib/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)$/;
Expand Down Expand Up @@ -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) {
Expand Down

0 comments on commit 133a022

Please sign in to comment.