Skip to content

Commit f1d8db5

Browse files
authored
Merge pull request jsonwebtoken#385 from panva/remote-jwk-lookup
better remote key fetch
2 parents ac906b8 + 1d709c2 commit f1d8db5

File tree

4 files changed

+73
-31
lines changed

4 files changed

+73
-31
lines changed

src/editor/public-key-download.js

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import jose from 'node-jose';
2+
13
import { httpGet } from '../utils.js';
24

35
function getKeyFromX5c(x5c) {
@@ -28,13 +30,13 @@ function getKeyFromX5Claims(claims) {
2830
} else {
2931
reject('x5c or x5u claims not available');
3032
}
31-
});
33+
});
3234
}
3335

3436
function getKeyFromJwkKeySetUrl(kid, url) {
3537
return httpGet(url).then(data => {
3638
data = JSON.parse(data);
37-
39+
3840
if(!data || !data.keys || !(data.keys instanceof Array)) {
3941
throw new Error(`Could not get JWK key set from URL: ${url}`);
4042
}
@@ -66,10 +68,31 @@ export function downloadPublicKeyIfPossible(decodedToken) {
6668
resolve(getKeyFromJwkKeySetUrl(header.kid, header.jku));
6769
} else if(header.jwk) {
6870
resolve(getKeyFromX5Claims(header.jwk));
69-
} else if(header.kid && payload.iss) {
70-
//Auth0-specific scheme
71-
const url = payload.iss + '.well-known/jwks.json';
72-
resolve(getKeyFromJwkKeySetUrl(header.kid, url));
71+
} else if(payload.iss) {
72+
const url = payload.iss + (payload.iss.substr(-1) === '/' ? '.well-known/openid-configuration' : '/.well-known/openid-configuration')
73+
74+
httpGet(url).then(data => {
75+
data = JSON.parse(data);
76+
77+
if(!data || !data.jwks_uri || typeof data.jwks_uri !== 'string') {
78+
throw new Error(`Could not get jwks_uri from URL: ${url}`);
79+
}
80+
81+
return httpGet(data.jwks_uri)
82+
}).then(data => {
83+
data = JSON.parse(data);
84+
85+
return jose.JWK.asKeyStore(data);
86+
}).then(jwks => {
87+
88+
const keys = jwks.all({ alg: header.alg, kid: header.kid, use: 'sig' })
89+
90+
if (keys.length !== 1) {
91+
throw new Error('Could not find a single definitive key in jwks_uri');
92+
}
93+
94+
resolve(keys[0].toPEM())
95+
}).catch(reject);
7396
} else {
7497
reject('No details about key');
7598
}

test/functional/editor.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -660,6 +660,13 @@ describe('Editor', function() {
660660
before(async function() {
661661
this.app = express();
662662

663+
this.app.get('/.well-known/openid-configuration', (req, res) => {
664+
res.set('Access-Control-Allow-Origin', '*');
665+
res.json({
666+
jwks_uri: 'http://localhost:3000/.well-known/jwks.json'
667+
});
668+
});
669+
663670
this.app.get('/.well-known/jwks.json', (req, res) => {
664671
res.set('Access-Control-Allow-Origin', '*');
665672
res.json(jwks);
@@ -715,7 +722,7 @@ describe('Editor', function() {
715722
const publicKey = await this.page.$eval('textarea[name="public-key"]',
716723
publicKeyElement => publicKeyElement.value);
717724

718-
expect(publicKey).to.include(jwks.keys[0].x5c[0]);
725+
expect(publicKey).to.include(`-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDdlatRjRjogo3WojgGHFHYLugd\nUWAY9iR3fy4arWNA1KoS8kVw33cJibXr8bvwUAUparCwlvdbH6dvEOfou0/gCFQs\nHUfQrSDv+MuSUMAe8jzKE4qW+jK+xQU9a03GUnKHkkle+Q0pX/g6jXZ7r1/xAK5D\no2kQ+X5xK9cipRgEKwIDAQAB\n-----END PUBLIC KEY-----\n`);
719726

720727
const valid = await this.page.$eval('.validation-status', status => {
721728
return status.classList.contains('valid-token') &&

test/functional/jwks.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88
"MIIDBjCCAe4CCQCu9p2owjWCFjANBgkqhkiG9w0BAQsFADBFMQswCQYDVQQGEwJVUzETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMB4XDTE5MDMxNjExMzYyOVoXDTIwMDMxNTExMzYyOVowRTELMAkGA1UEBhMCVVMxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJ88orNWY3zQdGwYChTEr75E7cJbwbGiau0ucAPpM3lTlaVVsJFnVYWuLN/FzP6Wv8q+O2r+/s91U5rw0cgB3Gk/dsIURBaS7/XI+ZU3iUom8q/zK5v2LYwmVVoGjmCIcK18Ci6j6/9dYp1rAJHyMrbx1k8WWBHFy4AFxblLmkt7hfYBIjUMMxk1Nb9BapKkwa+AfJ1txwjeO11LtLfGNHvpX+LODsUGsFg+/Sff+Xd0ctL21dwJtRbRiYibzsEbCH1QoQ6WErU3B0wjKrb1m1ei9dQVpKcxl0luB7+N6mvhkmDg9kFOvDG+faEpNjgfgbTi6SaH5mxhBoL5sMgiPTMCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAQItHIdKxBhnG6yqZIq9inhwQZJ2D/YV/FJfhLsHcvj/OnOvu/sZPn2SEwsi4w4LobS1MH2bxX15m+pK0sUdLxWysLr3afDEOIlI1pHPxPv4sXeJ2g/N2Wzr7ZMTEXSlCWgU+4Q9aIvNOlaAS5EBhQXTDZqwLer+tgeC+7da8rihuOnQWQraWGGXpKVHb2kbBpVZi61cjmvic6Caun4AZjL+UOEWpzPtFnvgTKJRD7Vh0fUfhxnKXHLAV/kcdcRPEeTg+ZDAWTThj20rDyxkWU87bptvI9VmxaummGwD/iknuiu84x3fiuEtwHbTCA4bz8ce6jzKQ2qBllvgfcVf4Dw=="
99
],
1010
"kid": "1",
11-
"x5t": "1"
11+
"x5t": "1",
12+
"e": "AQAB",
13+
"n": "3ZWrUY0Y6IKN1qI4BhxR2C7oHVFgGPYkd38uGq1jQNSqEvJFcN93CYm16_G78FAFKWqwsJb3Wx-nbxDn6LtP4AhULB1H0K0g7_jLklDAHvI8yhOKlvoyvsUFPWtNxlJyh5JJXvkNKV_4Oo12e69f8QCuQ6NpEPl-cSvXIqUYBCs"
1214
}
1315
]
1416
}

test/unit/editor/public-key-download.js

Lines changed: 33 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import _ from 'lodash';
44
import sinon from 'sinon';
55
import sinonChai from 'sinon-chai';
66

7-
import publicKeyDownloadInjector from
7+
import publicKeyDownloadInjector from
88
'inject-loader!../../../src/editor/public-key-download.js';
99

1010
chai.use(chaiAsPromised);
@@ -26,44 +26,54 @@ describe('Public key downloader', function() {
2626

2727
const jwks = {
2828
keys: [{
29-
kid: 1,
30-
x5c: ['test-x5c-key']
29+
kty: 'RSA',
30+
kid: '1',
31+
e: 'AQAB',
32+
n: '1GPz-Er5h7PCk4v3pSlnaLYNYrp4sVc6Tx7FVz9d8m4zIS2qzcTM_6dRbMgZ4hBdD35NpYzU4z-d8lN27-J_jOzHnCiMdkY-w52dCofAkICh6ftkFlG9bFQyH8Jz5UtpVkZyy1dxCRz_sbRAzUdjUYsGvrKXg-3UYCL5SBCnt0ycrvr3iKX9k8IlMrFRB8lBJ6eQVzkzGsuivPaThXjVZ_OpY7W-XsDjut7cFgPKIc843tW4CNaDJ6j3afm-RFOok__xLQH5uA7HXS_yqfEchvzXfYfMxJY2d-Eqw4xTurm3TT07RnwJuN9slDJUrTH9EKkJkjZ7dn7fZtGjGTpaDQ',
33+
x5c: ['test-x5c-key'],
3134
}]
3235
};
3336

37+
const keyAsPEM = `-----BEGIN PUBLIC KEY-----\r\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1GPz+Er5h7PCk4v3pSln\r\naLYNYrp4sVc6Tx7FVz9d8m4zIS2qzcTM/6dRbMgZ4hBdD35NpYzU4z+d8lN27+J/\r\njOzHnCiMdkY+w52dCofAkICh6ftkFlG9bFQyH8Jz5UtpVkZyy1dxCRz/sbRAzUdj\r\nUYsGvrKXg+3UYCL5SBCnt0ycrvr3iKX9k8IlMrFRB8lBJ6eQVzkzGsuivPaThXjV\r\nZ/OpY7W+XsDjut7cFgPKIc843tW4CNaDJ6j3afm+RFOok//xLQH5uA7HXS/yqfEc\r\nhvzXfYfMxJY2d+Eqw4xTurm3TT07RnwJuN9slDJUrTH9EKkJkjZ7dn7fZtGjGTpa\r\nDQIDAQAB\r\n-----END PUBLIC KEY-----\r\n`;
38+
3439
function httpGetMock(data) {
3540
return (url) => data ? Promise.resolve(data) : Promise.reject();
3641
}
3742

3843
it('Finds keys in iss + .well-known URL', function(done) {
3944
const decodedToken = _.defaultsDeep({}, decodedBaseToken, {
4045
header: {
41-
kid: 1
46+
kid: '1'
4247
},
4348
payload: {
4449
iss: baseUrl
4550
}
4651
});
4752

48-
const httpGetStub = sinon.stub().resolves(JSON.stringify(jwks));
53+
const httpGetStub = sinon.stub()
54+
.onCall(0).resolves(JSON.stringify({ jwks_uri: '/.well-known/jwks.json' }))
55+
.onCall(1).resolves(JSON.stringify(jwks));
56+
4957
const downloadPublicKeyIfPossible = publicKeyDownloadInjector({
5058
'../utils.js': {
5159
httpGet: httpGetStub
5260
}
5361
}).downloadPublicKeyIfPossible;
5462

5563
downloadPublicKeyIfPossible(decodedToken)
56-
.should.eventually.include(jwks.keys[0].x5c[0])
64+
.should.eventually.include(keyAsPEM)
5765
.then(() => {
66+
httpGetStub.should.have.been
67+
.calledWith(baseUrl + '.well-known/openid-configuration');
5868
httpGetStub.should.have.been
5969
.calledWith(baseUrl + '.well-known/jwks.json');
60-
}).should.notify(done);
70+
}).should.notify(done);
6171
});
6272

6373
it('Finds keys in jwk header claim', function(done) {
6474
const decodedToken = _.defaultsDeep({}, decodedBaseToken, {
6575
header: {
66-
kid: 1,
76+
kid: '1',
6777
jwk: jwks.keys[0]
6878
}
6979
});
@@ -79,13 +89,13 @@ describe('Public key downloader', function() {
7989
.should.eventually.include(jwks.keys[0].x5c[0])
8090
.then(() => {
8191
httpGetStub.should.have.callCount(0);
82-
}).should.notify(done);
92+
}).should.notify(done);
8393
});
8494

8595
it('Finds keys in jku header claim', function(done) {
8696
const decodedToken = _.defaultsDeep({}, decodedBaseToken, {
8797
header: {
88-
kid: 1,
98+
kid: '1',
8999
jku: baseUrl
90100
}
91101
});
@@ -101,7 +111,7 @@ describe('Public key downloader', function() {
101111
.should.eventually.include(jwks.keys[0].x5c[0])
102112
.then(() => {
103113
httpGetStub.should.have.been.calledWith(baseUrl);
104-
}).should.notify(done);
114+
}).should.notify(done);
105115
});
106116

107117
it('Finds keys in x5u header claim', function(done) {
@@ -122,7 +132,7 @@ describe('Public key downloader', function() {
122132
.should.eventually.include(jwks.keys[0].x5c[0])
123133
.then(() => {
124134
httpGetStub.should.have.been.calledWith(baseUrl);
125-
}).should.notify(done);
135+
}).should.notify(done);
126136
});
127137

128138
it('Finds keys in x5c string header claim', function(done) {
@@ -143,7 +153,7 @@ describe('Public key downloader', function() {
143153
.should.eventually.include(jwks.keys[0].x5c[0])
144154
.then(() => {
145155
httpGetStub.should.have.callCount(0);
146-
}).should.notify(done);
156+
}).should.notify(done);
147157
});
148158

149159
it('Finds keys in x5c array header claim', function(done) {
@@ -164,13 +174,13 @@ describe('Public key downloader', function() {
164174
.should.eventually.include(jwks.keys[0].x5c[0])
165175
.then(() => {
166176
httpGetStub.should.have.callCount(0);
167-
}).should.notify(done);
177+
}).should.notify(done);
168178
});
169179

170180
it('Rejects the promise when HTTP request fails', function(done) {
171181
const decodedToken = _.defaultsDeep({}, decodedBaseToken, {
172182
header: {
173-
kid: 1,
183+
kid: '1',
174184
jku: baseUrl
175185
}
176186
});
@@ -186,14 +196,14 @@ describe('Public key downloader', function() {
186196
.should.be.rejected
187197
.then(() => {
188198
httpGetStub.should.have.been.calledWith(baseUrl);
189-
}).should.notify(done);
199+
}).should.notify(done);
190200
});
191201

192-
describe('Rejects the promise when invalid data ' +
202+
describe('Rejects the promise when invalid data ' +
193203
'is in jku claim URL', function() {
194204
const decodedToken = _.defaultsDeep({}, decodedBaseToken, {
195205
header: {
196-
kid: 1,
206+
kid: '1',
197207
jku: baseUrl
198208
}
199209
});
@@ -216,9 +226,9 @@ describe('Public key downloader', function() {
216226
httpGetStub.should.have.been.calledWith(baseUrl);
217227
}).should.notify(done);
218228
});
219-
229+
220230
it('when the keys object does not exist', function(done) {
221-
httpGetStub = sinon.stub().resolves({
231+
httpGetStub = sinon.stub().resolves({
222232
});
223233

224234
downloadPublicKeyIfPossible(decodedToken)
@@ -227,7 +237,7 @@ describe('Public key downloader', function() {
227237
httpGetStub.should.have.been.calledWith(baseUrl);
228238
}).should.notify(done);
229239
});
230-
240+
231241
it('when there is no kid', function(done) {
232242
httpGetStub = sinon.stub().resolves({
233243
keys: [{
@@ -241,11 +251,11 @@ describe('Public key downloader', function() {
241251
httpGetStub.should.have.been.calledWith(baseUrl);
242252
}).should.notify(done);
243253
});
244-
254+
245255
it('when there are no x5u or x5c claims', function(done) {
246256
httpGetStub = sinon.stub().resolves({
247257
keys: [{
248-
kid: 1
258+
kid: '1'
249259
}]
250260
});
251261

0 commit comments

Comments
 (0)