Skip to content

Commit

Permalink
Merge pull request IdentityModel#559 from Prithvirajbilla/dev
Browse files Browse the repository at this point in the history
Adds retry behavior while fetching JWKS keys.
  • Loading branch information
brockallen authored Dec 8, 2020
2 parents e61cf89 + 3f01bb6 commit 5ef0f4c
Show file tree
Hide file tree
Showing 4 changed files with 111 additions and 30 deletions.
5 changes: 5 additions & 0 deletions src/MetadataService.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ export class MetadataService {
return this._metadataUrl;
}

resetSigningKeys() {
this._settings = this._settings || {}
this._settings.signingKeys = undefined
}

getMetadata() {
if (this._settings.metadata) {
Log.debug("MetadataService.getMetadata: Returning metadata from settings");
Expand Down
74 changes: 44 additions & 30 deletions src/ResponseValidator.js
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,49 @@ export class ResponseValidator {
});
}

_getSigningKeyForJwt(jwt) {
return this._metadataService.getSigningKeys().then(keys => {
const kid = jwt.header.kid;
if (!keys) {
Log.error("ResponseValidator._validateIdToken: No signing keys from metadata");
return Promise.reject(new Error("No signing keys from metadata"));
}

Log.debug("ResponseValidator._validateIdToken: Received signing keys");
let key;
if (!kid) {
keys = this._filterByAlg(keys, jwt.header.alg);

if (keys.length > 1) {
Log.error("ResponseValidator._validateIdToken: No kid found in id_token and more than one key found in metadata");
return Promise.reject(new Error("No kid found in id_token and more than one key found in metadata"));
} else {
// kid is mandatory only when there are multiple keys in the referenced JWK Set document
// see http://openid.net/specs/openid-connect-core-1_0.html#Signing
key = keys[0];
}
} else {
key = keys.filter(key => {
return key.kid === kid;
})[0];
}
return Promise.resolve(key);
});
}

_getSigningKeyForJwtWithSingleRetry(jwt) {
return this._getSigningKeyForJwt(jwt).then(key => {
// Refreshing signingKeys if no suitable verification key is present for given jwt header.
if (!key) {
// set to undefined, to trigger network call to jwks_uri.
this._metadataService.resetSigningKeys();
return this._getSigningKeyForJwt(jwt);
} else {
return Promise.resolve(key);
}
});
}

_validateIdToken(state, response) {
if (!state.nonce) {
Log.error("ResponseValidator._validateIdToken: No nonce on state");
Expand All @@ -325,38 +368,9 @@ export class ResponseValidator {
return Promise.reject(new Error("Invalid nonce in id_token"));
}

var kid = jwt.header.kid;

return this._metadataService.getIssuer().then(issuer => {
Log.debug("ResponseValidator._validateIdToken: Received issuer");

return this._metadataService.getSigningKeys().then(keys => {
if (!keys) {
Log.error("ResponseValidator._validateIdToken: No signing keys from metadata");
return Promise.reject(new Error("No signing keys from metadata"));
}

Log.debug("ResponseValidator._validateIdToken: Received signing keys");
let key;
if (!kid) {
keys = this._filterByAlg(keys, jwt.header.alg);

if (keys.length > 1) {
Log.error("ResponseValidator._validateIdToken: No kid found in id_token and more than one key found in metadata");
return Promise.reject(new Error("No kid found in id_token and more than one key found in metadata"));
}
else {
// kid is mandatory only when there are multiple keys in the referenced JWK Set document
// see http://openid.net/specs/openid-connect-core-1_0.html#Signing
key = keys[0];
}
}
else {
key = keys.filter(key => {
return key.kid === kid;
})[0];
}

return this._getSigningKeyForJwtWithSingleRetry(jwt).then(key => {
if (!key) {
Log.error("ResponseValidator._validateIdToken: No key matching kid or alg found in signing keys");
return Promise.reject(new Error("No key matching kid or alg found in signing keys"));
Expand Down
61 changes: 61 additions & 0 deletions test/unit/ResponseValidator.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,15 @@ class MockResponseValidator extends ResponseValidator {
return this._mock("_mergeClaims", ...args);
}

_getSigningKeyForJwt(...args) {
this._getSigningKeyForJwtSignedCalledCount = (this._getSigningKeyForJwtSignedCalledCount || 0) + 1;
return this._mock("_getSigningKeyForJwt", ...args);
}
_getSigningKeyForJwtWithSingleRetry(...args) {
this._getSigningKeyForJwtSignedCalledCount = 0;
return this._mock("_getSigningKeyForJwtWithSingleRetry", ...args);
}

_validateIdTokenAndAccessToken(...args) {
return this._mock("_validateIdTokenAndAccessToken", ...args);
}
Expand Down Expand Up @@ -716,6 +725,58 @@ describe("ResponseValidator", function () {

});

describe("_getSigningKeyForJwt", function () {

it("should fail if loading keys fails.", function (done) {

const jwt = { header: { kid: 'a3rMUgMFv9tPclLa6yF3zAkfquE' }};
stubMetadataService.getSigningKeysResult = Promise.reject(new Error("keys"));

subject._getSigningKeyForJwt(jwt).then(null, err => {
err.message.should.contain('keys');
done();
})
})

it("should fetch suitable signing key for the jwt.", function (done) {

const jwt = { header: { kid: 'a3rMUgMFv9tPclLa6yF3zAkfquE' }};
stubMetadataService.getSigningKeysResult = Promise.resolve([{ kid: 'a3rMUgMFv9tPclLa6yF3zAkfquE' }, { kid: 'other_key' } ])

subject._getSigningKeyForJwt(jwt).then(key => {
key.should.deep.equal({ kid: 'a3rMUgMFv9tPclLa6yF3zAkfquE' })
done();
})
})
})

describe("_getSigningKeyForJwtWithSingleRetry", function () {

it("should retry once if suitable signing key is not found.", function (done) {

const jwt = { header: { kid: 'a3rMUgMFv9tPclLa6yF3zAkfquE' }};
var callCount = 0
stubMetadataService.getSigningKeysResult = Promise.resolve([ { kid: 'other_key' } ])

subject._getSigningKeyForJwtWithSingleRetry(jwt).then(key => {
subject._getSigningKeyForJwtSignedCalledCount.should.equal(2);
done();
})
})

it("should not retry if suitable signing key is found.", function (done) {

const jwt = { header: { kid: 'a3rMUgMFv9tPclLa6yF3zAkfquE' }};
var callCount = 0
stubMetadataService.getSigningKeysResult = Promise.resolve([ { kid: 'a3rMUgMFv9tPclLa6yF3zAkfquE' } ])

subject._getSigningKeyForJwtWithSingleRetry(jwt).then(key => {
subject._getSigningKeyForJwtSignedCalledCount.should.equal(1);
done();
})
})
})

describe("_validateIdToken", function () {

it("should fail if no nonce on state", function (done) {
Expand Down
1 change: 1 addition & 0 deletions test/unit/StubMetadataService.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.

export class StubMetadataService{
resetSigningKeys(){ }
getMetadata(){
return this.getMetadataResult;
}
Expand Down

0 comments on commit 5ef0f4c

Please sign in to comment.