Skip to content

Commit

Permalink
feat: update DPoP support to draft-03 (#407)
Browse files Browse the repository at this point in the history
Resolves #406 

Co-authored-by: Richard L. Barnes <richbarn@cisco.com>
Co-authored-by: Filip Skokan <panva.ip@gmail.com>
  • Loading branch information
3 people committed Sep 20, 2021
1 parent 2a84e46 commit 5565ee1
Show file tree
Hide file tree
Showing 4 changed files with 33 additions and 9 deletions.
4 changes: 2 additions & 2 deletions README.md
Expand Up @@ -48,7 +48,7 @@ openid-client.
- RP-Initiated Logout
- [Financial-grade API - Part 2: Read and Write API Security Profile (FAPI) - ID2][feature-fapi]
- [JWT Secured Authorization Response Mode for OAuth 2.0 (JARM) - ID1][feature-jarm]
- [OAuth 2.0 Demonstration of Proof-of-Possession at the Application Layer (DPoP) - draft 01][feature-dpop]
- [OAuth 2.0 Demonstration of Proof-of-Possession at the Application Layer (DPoP) - draft 03][feature-dpop]

Updates to draft specifications (DPoP, JARM, and FAPI) are released as MINOR library versions,
if you utilize these specification implementations consider using the tilde `~` operator in your
Expand Down Expand Up @@ -297,7 +297,7 @@ See [Customizing (docs)](https://github.com/panva/node-openid-client/blob/master
[feature-rp-logout]: https://openid.net/specs/openid-connect-session-1_0.html#RPLogout
[feature-jarm]: https://openid.net/specs/openid-financial-api-jarm-ID1.html
[feature-fapi]: https://openid.net/specs/openid-financial-api-part-2-ID2.html
[feature-dpop]: https://tools.ietf.org/html/draft-ietf-oauth-dpop-01
[feature-dpop]: https://tools.ietf.org/html/draft-ietf-oauth-dpop-03
[feature-par]: https://www.rfc-editor.org/rfc/rfc9126.html
[feature-jar]: https://www.rfc-editor.org/rfc/rfc9101.html
[openid-certified-link]: https://openid.net/certification/
Expand Down
15 changes: 11 additions & 4 deletions lib/client.js
Expand Up @@ -1013,8 +1013,9 @@ module.exports = (issuer, aadIssValidation = false) => class Client extends Base
method,
headers,
body,
tokenType = accessToken instanceof TokenSet ? accessToken.token_type : 'Bearer',
DPoP,
// eslint-disable-next-line no-nested-ternary
tokenType = DPoP ? 'DPoP' : accessToken instanceof TokenSet ? accessToken.token_type : 'Bearer',
} = {},
) {
if (accessToken instanceof TokenSet) {
Expand All @@ -1039,7 +1040,7 @@ module.exports = (issuer, aadIssValidation = false) => class Client extends Base
responseType: 'buffer',
method,
url: resourceUrl,
}, { mTLS, DPoP });
}, { accessToken, mTLS, DPoP });
}

/**
Expand Down Expand Up @@ -1659,7 +1660,7 @@ Object.defineProperty(BaseClient.prototype, 'validateJARM', {
* @name dpopProof
* @api private
*/
function dpopProof(payload, jwk) {
function dpopProof(payload, jwk, accessToken) {
if (!isPlainObject(payload)) {
throw new TypeError('payload must be a plain object');
}
Expand All @@ -1683,9 +1684,15 @@ function dpopProof(payload, jwk) {
[alg] = key.algorithms('sign');
}

let ath;
if (accessToken) {
ath = base64url.encode(crypto.createHash('sha256').update(accessToken).digest());
}

return jose.JWS.sign({
iat: now(),
jti: random(),
ath,
...payload,
}, jwk, {
alg,
Expand All @@ -1699,7 +1706,7 @@ Object.defineProperty(BaseClient.prototype, 'dpopProof', {
configurable: true,
value(...args) {
process.emitWarning(
'The DPoP APIs implements an IETF draft. 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.',
'The DPoP APIs implements an IETF draft (https://www.ietf.org/archive/id/draft-ietf-oauth-dpop-03.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', {
Expand Down
4 changes: 2 additions & 2 deletions lib/helpers/request.js
Expand Up @@ -22,7 +22,7 @@ setDefaults({
throwHttpErrors: false,
});

module.exports = async function request(options, { mTLS = false, DPoP } = {}) {
module.exports = async function request(options, { accessToken, mTLS = false, DPoP } = {}) {
const { url } = options;
isAbsoluteUrl(url);
const optsFn = this[HTTP_OPTIONS];
Expand All @@ -33,7 +33,7 @@ module.exports = async function request(options, { mTLS = false, DPoP } = {}) {
opts.headers.DPoP = this.dpopProof({
htu: url,
htm: options.method,
}, DPoP);
}, DPoP, accessToken);
}

if (optsFn) {
Expand Down
19 changes: 18 additions & 1 deletion test/client/dpop.test.js
Expand Up @@ -48,7 +48,7 @@ describe('DPoP', () => {
expect(() => this.client.dpopProof({}, jose.JWK.generateSync('EC').toPEM())).to.throw(msg);
});

it('DPoP Proof JWT', function () {
it('DPoP Proof JWT w/o ath', function () {
const proof = this.client.dpopProof({
htu: 'foo',
htm: 'bar',
Expand Down Expand Up @@ -80,6 +80,15 @@ describe('DPoP', () => {
}
});

it('DPoP Proof JWT w/ ath', function () {
const proof = this.client.dpopProof({
htu: 'foo',
htm: 'bar',
}, jose.JWK.generateSync('EC'), 'foo');
const decoded = jose.JWT.decode(proof, { complete: true });
expect(decoded).to.have.nested.property('payload.ath', 'LCa0a2j_xo_5m0U8HTBBNBNCLXBkg7-g-YpeiGJm564');
});

it('key.alg is used if present', function () {
const proof = this.client.dpopProof({}, jose.JWK.generateSync('RSA', 2048, { alg: 'PS384' }));
expect(jose.JWT.decode(proof, { complete: true })).to.have.nested.property('header.alg', 'PS384');
Expand All @@ -103,6 +112,10 @@ describe('DPoP', () => {
await this.client.userinfo('foo', { DPoP: jwk });

expect(this.httpOpts).to.have.nested.property('headers.DPoP');

const proof = this.httpOpts.headers.DPoP;
const proofJWT = jose.JWT.decode(proof, { complete: true });
expect(proofJWT).to.have.nested.property('payload.ath');
});

it('is enabled for requestResource', async function () {
Expand All @@ -112,6 +125,10 @@ describe('DPoP', () => {
await this.client.requestResource('https://rs.example.com/resource', 'foo', { DPoP: jwk, method: 'POST' });

expect(this.httpOpts).to.have.nested.property('headers.DPoP');

const proof = this.httpOpts.headers.DPoP;
const proofJWT = jose.JWT.decode(proof, { complete: true });
expect(proofJWT).to.have.nested.property('payload.ath');
});

it('is enabled for grant', async function () {
Expand Down

0 comments on commit 5565ee1

Please sign in to comment.