Skip to content

Commit c391207

Browse files
committed
add clock skew feature to JWT validation
1 parent 94da965 commit c391207

File tree

7 files changed

+155
-28
lines changed

7 files changed

+155
-28
lines changed

dist/oidc-client.js

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "oidc-client",
3-
"version": "1.0.0-beta.3",
3+
"version": "1.0.0-beta.4",
44
"description": "OpenID Connect (OIDC) & OAuth2 client library",
55
"main": "index.js",
66
"scripts": {

src/JoseUtil.js

Lines changed: 53 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import { jws, KEYUTIL as KeyUtil, X509, crypto, hextob64u } from 'jsrsasign';
55
import Log from './Log';
66

7+
const AllowedSigningAlgs = ['RS256', 'RS384', 'RS512', 'PS256', 'PS384', 'PS512', 'ES256', 'ES384', 'ES512'];
8+
79
export default class JoseUtil {
810

911
static parseJwt(jwt) {
@@ -20,7 +22,7 @@ export default class JoseUtil {
2022
}
2123
}
2224

23-
static validateJwt(jwt, key, issuer, audience, now) {
25+
static validateJwt(jwt, key, issuer, audience, clockSkew, now) {
2426
Log.info("JoseUtil.validateJwt");
2527

2628
try {
@@ -50,14 +52,7 @@ export default class JoseUtil {
5052
return false;
5153
}
5254

53-
return jws.JWS.verifyJWT(jwt,
54-
key,
55-
{
56-
alg: ['RS256', 'RS384', 'RS512', 'PS256', 'PS384', 'PS512', 'ES256', 'ES384', 'ES512'],
57-
iss: [issuer],
58-
aud: [audience],
59-
verifyAt: now
60-
});
55+
return JoseUtil._validateJwt(jwt, key, issuer, audience, clockSkew, now);
6156
}
6257
catch (e) {
6358
Log.error(e);
@@ -66,6 +61,55 @@ export default class JoseUtil {
6661
return false;
6762
}
6863

64+
static _validateJwt(jwt, key, issuer, audience, clockSkew, now) {
65+
Log.info("JoseUtil._validateJwt");
66+
67+
if (!clockSkew) {
68+
clockSkew = 0;
69+
}
70+
71+
if (!now) {
72+
now = parseInt(Date.now() / 1000);
73+
}
74+
75+
var payload = JoseUtil.parseJwt(jwt).payload;
76+
77+
if (payload.iss !== issuer) {
78+
Log.error("Invalid issuer in token", payload.iss);
79+
return false;
80+
}
81+
82+
if (payload.aud !== audience) {
83+
Log.error("Invalid audience in token", payload.aud);
84+
return false;
85+
}
86+
87+
var lowerNow = now + clockSkew;
88+
var upperNow = now - clockSkew;
89+
90+
if (lowerNow < payload.iat) {
91+
Log.error("Issued at value is in the future", payload.iat);
92+
return false;
93+
}
94+
95+
if (lowerNow < payload.nbf) {
96+
Log.error("Not before time is in the future", payload.nbf);
97+
return false;
98+
}
99+
100+
if (payload.exp < upperNow) {
101+
Log.error("Expiration is in the past", payload.exp);
102+
return false;
103+
}
104+
105+
if (!jws.JWS.verify(jwt, key, AllowedSigningAlgs)) {
106+
Log.error("Signature validation failed");
107+
return false;
108+
}
109+
110+
return true;
111+
}
112+
69113
static hashString(value, alg) {
70114
Log.info("JoseUtil.hashString", value, alg);
71115
try {

src/OidcClientSettings.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const OidcMetadataUrlPath = '.well-known/openid-configuration';
88
const DefaultResponseType = "id_token";
99
const DefaultScope = "openid";
1010
const DefaultStaleStateAge = 60; // seconds
11+
const DefaultClockSkewInSeconds = 60 * 5;
1112

1213
export default class OidcClientSettings {
1314
constructor({
@@ -19,7 +20,8 @@ export default class OidcClientSettings {
1920
// optional protocol
2021
prompt, display, max_age, ui_locales, acr_values,
2122
// behavior flags
22-
filterProtocolClaims = true, loadUserInfo = true, staleStateAge = DefaultStaleStateAge
23+
filterProtocolClaims = true, loadUserInfo = true,
24+
staleStateAge = DefaultStaleStateAge, clockSkew = DefaultClockSkewInSeconds
2325
} = {}) {
2426

2527
this._authority = authority;
@@ -42,6 +44,7 @@ export default class OidcClientSettings {
4244
this._filterProtocolClaims = !!filterProtocolClaims;
4345
this._loadUserInfo = !!loadUserInfo;
4446
this._staleStateAge = staleStateAge;
47+
this._clockSkew = clockSkew;
4548
}
4649

4750
// client config
@@ -114,7 +117,6 @@ export default class OidcClientSettings {
114117
this._signingKeys = value;
115118
}
116119

117-
118120
// behavior flags
119121
get filterProtocolClaims() {
120122
return this._filterProtocolClaims;
@@ -125,4 +127,7 @@ export default class OidcClientSettings {
125127
get staleStateAge() {
126128
return this._staleStateAge;
127129
}
130+
get clockSkew() {
131+
return this._clockSkew;
132+
}
128133
}

src/ResponseValidator.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,8 +244,11 @@ export default class ResponseValidator {
244244
Log.error("No key matching kid found in signing keys");
245245
return Promise.reject(new Error("No key matching kid found in signing keys"));
246246
}
247+
248+
let clockSkewInSeconds = this._settings.clockSkew;
249+
Log.info("Validaing JWT; using clock skew (in seconds) of: ", clockSkewInSeconds);
247250

248-
if (!this._joseUtil.validateJwt(response.id_token, key, issuer, audience)) {
251+
if (!this._joseUtil.validateJwt(response.id_token, key, issuer, audience, clockSkewInSeconds)) {
249252
Log.error("JWT failed to validate");
250253
return Promise.reject(new Error("JWT failed to validate"));
251254
}

test/unit/JoseUtil.spec.js

Lines changed: 69 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,16 @@ describe("JoseUtil", function() {
1818

1919
const expectedIssuer = "https://localhost:44333/core";
2020
const expectedAudience = "js.tokenmanager";
21-
const expires = 1459130201;
2221
const notBefore = 1459129901;
22+
const issuedAt = notBefore;
23+
const expires = 1459130201;
24+
2325
const expectedNow = notBefore;
2426

2527
beforeEach(function() {
2628
Log.logger = console;
2729
Log.level = Log.NONE;
28-
30+
2931
jwt = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6ImEzck1VZ01Gdjl0UGNsTGE2eUYzekFrZnF1RSIsImtpZCI6ImEzck1VZ01Gdjl0UGNsTGE2eUYzekFrZnF1RSJ9.eyJpc3MiOiJodHRwczovL2xvY2FsaG9zdDo0NDMzMy9jb3JlIiwiYXVkIjoianMudG9rZW5tYW5hZ2VyIiwiZXhwIjoxNDU5MTMwMjAxLCJuYmYiOjE0NTkxMjk5MDEsIm5vbmNlIjoiNzIyMTAwNTIwOTk3MjM4MiIsImlhdCI6MTQ1OTEyOTkwMSwiYXRfaGFzaCI6IkpnRFVDeW9hdEp5RW1HaWlXYndPaEEiLCJzaWQiOiIwYzVmMDYxZTYzOThiMWVjNmEwYmNlMmM5NDFlZTRjNSIsInN1YiI6Ijg4NDIxMTEzIiwiYXV0aF90aW1lIjoxNDU5MTI5ODk4LCJpZHAiOiJpZHNydiIsImFtciI6WyJwYXNzd29yZCJdfQ.f6S1Fdd0UQScZAFBzXwRiVsUIPQnWZLSe07kdtjANRZDZXf5A7yDtxOftgCx5W0ONQcDFVpLGPgTdhp7agZkPpCFutzmwr0Rr9G7E7mUN4xcIgAABhmRDfzDayFBEu6VM8wEWTChezSWtx2xG_2zmVJxxmNV0jvkaz0bu7iin-C_UZg6T-aI9FZDoKRGXZP9gF65FQ5pQ4bCYQxhKcvjjUfs0xSHGboL7waN6RfDpO4vvVR1Kz-PQhIRyFAJYRuoH4PdMczHYtFCb-k94r-7TxEU0vp61ww4WntbPvVWwUbCUgsEtmDzAZT-NEJVhWztNk1ip9wDPXzZ2hEhDAPJ7A";
3032

3133
jwtFromRsa = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6ImEzck1VZ01Gdjl0UGNsTGE2eUYzekFrZnF1RSIsImtpZCI6ImEzck1VZ01Gdjl0UGNsTGE2eUYzekFrZnF1RSJ9.eyJpc3MiOiJodHRwczovL2xvY2FsaG9zdDo0NDMzMy9jb3JlIiwiYXVkIjoianMudG9rZW5tYW5hZ2VyIiwiZXhwIjoxNDU5MTMwMjAxLCJuYmYiOjE0NTkxMjk5MDEsIm5vbmNlIjoiNzIyMTAwNTIwOTk3MjM4MiIsImlhdCI6MTQ1OTEyOTkwMSwiYXRfaGFzaCI6IkpnRFVDeW9hdEp5RW1HaWlXYndPaEEiLCJzaWQiOiIwYzVmMDYxZTYzOThiMWVjNmEwYmNlMmM5NDFlZTRjNSIsInN1YiI6Ijg4NDIxMTEzIiwiYXV0aF90aW1lIjoxNDU5MTI5ODk4LCJpZHAiOiJpZHNydiIsImFtciI6WyJwYXNzd29yZCJdfQ.f6S1Fdd0UQScZAFBzXwRiVsUIPQnWZLSe07kdtjANRZDZXf5A7yDtxOftgCx5W0ONQcDFVpLGPgTdhp7agZkPpCFutzmwr0Rr9G7E7mUN4xcIgAABhmRDfzDayFBEu6VM8wEWTChezSWtx2xG_2zmVJxxmNV0jvkaz0bu7iin-C_UZg6T-aI9FZDoKRGXZP9gF65FQ5pQ4bCYQxhKcvjjUfs0xSHGboL7waN6RfDpO4vvVR1Kz-PQhIRyFAJYRuoH4PdMczHYtFCb-k94r-7TxEU0vp61ww4WntbPvVWwUbCUgsEtmDzAZT-NEJVhWztNk1ip9wDPXzZ2hEhDAPJ7A";
@@ -107,7 +109,7 @@ describe("JoseUtil", function() {
107109
delete rsaKey.n;
108110
delete rsaKey.e;
109111

110-
var result = JoseUtil.validateJwt(jwtFromRsa, rsaKey, expectedIssuer, expectedAudience, expectedNow);
112+
var result = JoseUtil.validateJwt(jwtFromRsa, rsaKey, expectedIssuer, expectedAudience, 0, expectedNow);
111113
result.should.be.true;
112114

113115
});
@@ -118,7 +120,7 @@ describe("JoseUtil", function() {
118120

119121
delete rsaKey.x5c;
120122

121-
var result = JoseUtil.validateJwt(jwtFromRsa, rsaKey, expectedIssuer, expectedAudience, expectedNow);
123+
var result = JoseUtil.validateJwt(jwtFromRsa, rsaKey, expectedIssuer, expectedAudience, 0, expectedNow);
122124
result.should.be.true;
123125

124126
});
@@ -127,42 +129,97 @@ describe("JoseUtil", function() {
127129

128130
rsaKey.kty = "foo";
129131

130-
var result = JoseUtil.validateJwt(jwtFromRsa, rsaKey, expectedIssuer, expectedAudience, expectedNow);
132+
var result = JoseUtil.validateJwt(jwtFromRsa, rsaKey, expectedIssuer, expectedAudience, 0, expectedNow);
131133
result.should.be.false;
132134

133135
});
134136

135137
it("should fail for mismatched keys", function() {
136138

137-
var result = JoseUtil.validateJwt(jwtFromRsa, ecKey, expectedIssuer, expectedAudience, expectedNow);
139+
var result = JoseUtil.validateJwt(jwtFromRsa, ecKey, expectedIssuer, expectedAudience, 0, expectedNow);
138140
result.should.be.false;
139141

140142
});
141143

142-
it("should not validate after exp", function() {
144+
it("should not validate before nbf", function() {
143145

144-
var result = JoseUtil.validateJwt(jwtFromRsa, rsaKey, expectedIssuer, expectedAudience, expires + 1);
146+
var result = JoseUtil.validateJwt(jwtFromRsa, rsaKey, expectedIssuer, expectedAudience, 0, notBefore - 1);
145147
result.should.be.false;
146148

147149
});
150+
151+
it("should allow nbf within clock skew", function() {
152+
153+
var result = JoseUtil.validateJwt(jwtFromRsa, rsaKey, expectedIssuer, expectedAudience, 10, notBefore - 1);
154+
result.should.be.true;
148155

149-
it("should not validate before nbf", function() {
156+
var result = JoseUtil.validateJwt(jwtFromRsa, rsaKey, expectedIssuer, expectedAudience, 10, notBefore - 10);
157+
result.should.be.true;
158+
});
150159

151-
var result = JoseUtil.validateJwt(jwtFromRsa, rsaKey, expectedIssuer, expectedAudience, notBefore - 1);
160+
it("should now allow nbf outside clock skew", function() {
161+
162+
var result = JoseUtil.validateJwt(jwtFromRsa, rsaKey, expectedIssuer, expectedAudience, 10, notBefore - 11);
152163
result.should.be.false;
153164

154165
});
166+
167+
it("should not validate before iat", function() {
168+
169+
var result = JoseUtil.validateJwt(jwtFromRsa, rsaKey, expectedIssuer, expectedAudience, 0, issuedAt - 1);
170+
result.should.be.false;
171+
172+
});
173+
174+
it("should allow iat within clock skew", function() {
175+
176+
var result = JoseUtil.validateJwt(jwtFromRsa, rsaKey, expectedIssuer, expectedAudience, 10, issuedAt - 1);
177+
result.should.be.true;
178+
179+
var result = JoseUtil.validateJwt(jwtFromRsa, rsaKey, expectedIssuer, expectedAudience, 10, issuedAt - 10);
180+
result.should.be.true;
181+
});
155182

183+
it("should now allow iat outside clock skew", function() {
184+
185+
var result = JoseUtil.validateJwt(jwtFromRsa, rsaKey, expectedIssuer, expectedAudience, 10, issuedAt - 11);
186+
result.should.be.false;
187+
188+
});
189+
190+
it("should not validate after exp", function() {
191+
192+
var result = JoseUtil.validateJwt(jwtFromRsa, rsaKey, expectedIssuer, expectedAudience, 0, expires + 1);
193+
result.should.be.false;
194+
195+
});
196+
197+
it("should allow exp within clock skew", function() {
198+
199+
var result = JoseUtil.validateJwt(jwtFromRsa, rsaKey, expectedIssuer, expectedAudience, 10, expires + 1);
200+
result.should.be.true;
201+
202+
var result = JoseUtil.validateJwt(jwtFromRsa, rsaKey, expectedIssuer, expectedAudience, 10, expires + 10);
203+
result.should.be.true;
204+
});
205+
206+
it("should now allow exp outside clock skew", function() {
207+
208+
var result = JoseUtil.validateJwt(jwtFromRsa, rsaKey, expectedIssuer, expectedAudience, 10, expires + 11);
209+
result.should.be.false;
210+
211+
});
212+
156213
it("should not validate for invalid audience", function() {
157214

158-
var result = JoseUtil.validateJwt(jwtFromRsa, rsaKey, expectedIssuer, "invalid aud", expectedNow);
215+
var result = JoseUtil.validateJwt(jwtFromRsa, rsaKey, expectedIssuer, "invalid aud", 0, expectedNow);
159216
result.should.be.false;
160217

161218
});
162219

163220
it("should not validate for invalid issuer", function() {
164221

165-
var result = JoseUtil.validateJwt(jwtFromRsa, rsaKey, "invalid issuer", expectedAudience, expectedNow);
222+
var result = JoseUtil.validateJwt(jwtFromRsa, rsaKey, "invalid issuer", expectedAudience, 0, expectedNow);
166223
result.should.be.false;
167224

168225
});

test/unit/OidcClientSettings.spec.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,5 +269,23 @@ describe("OidcClientSettings", function() {
269269
subject.staleStateAge.should.equal(100);
270270
});
271271
});
272+
273+
describe("clockSkew", function() {
274+
275+
it("should use default value", function() {
276+
let subject = new OidcClientSettings({
277+
client_id: 'client'
278+
});
279+
subject.clockSkew.should.equal(5 * 60); // 5 mins
280+
});
281+
282+
it("should return value from initial settings", function() {
283+
let subject = new OidcClientSettings({
284+
client_id: 'client',
285+
clockSkew : 10
286+
});
287+
subject.clockSkew.should.equal(10);
288+
});
289+
});
272290

273291
});

0 commit comments

Comments
 (0)