Skip to content

Commit 5bbc961

Browse files
authored
Merge pull request DuendeArchive#636 from asleire/dev
Add UserInfo JWT response support
2 parents ad4d153 + b42ae57 commit 5bbc961

File tree

5 files changed

+163
-29
lines changed

5 files changed

+163
-29
lines changed

index.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ export interface OidcClientSettings {
140140
readonly staleStateAge?: number;
141141
readonly clockSkew?: number;
142142
readonly stateStore?: StateStore;
143+
readonly userInfoJwtIssuer?: 'ANY' | 'OP' | string;
143144
ResponseValidatorCtor?: ResponseValidatorCtor;
144145
MetadataServiceCtor?: MetadataServiceCtor;
145146
extraQueryParams?: {};

src/JoseUtil.js

Lines changed: 28 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export class JoseUtil {
2323
}
2424
}
2525

26-
static validateJwt(jwt, key, issuer, audience, clockSkew, now) {
26+
static validateJwt(jwt, key, issuer, audience, clockSkew, now, timeInsensitive) {
2727
Log.debug("JoseUtil.validateJwt");
2828

2929
try {
@@ -54,15 +54,15 @@ export class JoseUtil {
5454
return Promise.reject(new Error("Unsupported key type: " + key && key.kty));
5555
}
5656

57-
return JoseUtil._validateJwt(jwt, key, issuer, audience, clockSkew, now);
57+
return JoseUtil._validateJwt(jwt, key, issuer, audience, clockSkew, now, timeInsensitive);
5858
}
5959
catch (e) {
6060
Log.error(e && e.message || e);
6161
return Promise.reject("JWT validation failed");
6262
}
6363
}
6464

65-
static validateJwtAttributes(jwt, issuer, audience, clockSkew, now) {
65+
static validateJwtAttributes(jwt, issuer, audience, clockSkew, now, timeInsensitive) {
6666
if (!clockSkew) {
6767
clockSkew = 0;
6868
}
@@ -96,38 +96,40 @@ export class JoseUtil {
9696
return Promise.reject(new Error("Invalid azp in token: " + payload.azp));
9797
}
9898

99-
var lowerNow = now + clockSkew;
100-
var upperNow = now - clockSkew;
99+
if (!timeInsensitive) {
100+
var lowerNow = now + clockSkew;
101+
var upperNow = now - clockSkew;
101102

102-
if (!payload.iat) {
103-
Log.error("JoseUtil._validateJwt: iat was not provided");
104-
return Promise.reject(new Error("iat was not provided"));
105-
}
106-
if (lowerNow < payload.iat) {
107-
Log.error("JoseUtil._validateJwt: iat is in the future", payload.iat);
108-
return Promise.reject(new Error("iat is in the future: " + payload.iat));
109-
}
103+
if (!payload.iat) {
104+
Log.error("JoseUtil._validateJwt: iat was not provided");
105+
return Promise.reject(new Error("iat was not provided"));
106+
}
107+
if (lowerNow < payload.iat) {
108+
Log.error("JoseUtil._validateJwt: iat is in the future", payload.iat);
109+
return Promise.reject(new Error("iat is in the future: " + payload.iat));
110+
}
110111

111-
if (payload.nbf && lowerNow < payload.nbf) {
112-
Log.error("JoseUtil._validateJwt: nbf is in the future", payload.nbf);
113-
return Promise.reject(new Error("nbf is in the future: " + payload.nbf));
114-
}
112+
if (payload.nbf && lowerNow < payload.nbf) {
113+
Log.error("JoseUtil._validateJwt: nbf is in the future", payload.nbf);
114+
return Promise.reject(new Error("nbf is in the future: " + payload.nbf));
115+
}
115116

116-
if (!payload.exp) {
117-
Log.error("JoseUtil._validateJwt: exp was not provided");
118-
return Promise.reject(new Error("exp was not provided"));
119-
}
120-
if (payload.exp < upperNow) {
121-
Log.error("JoseUtil._validateJwt: exp is in the past", payload.exp);
122-
return Promise.reject(new Error("exp is in the past:" + payload.exp));
117+
if (!payload.exp) {
118+
Log.error("JoseUtil._validateJwt: exp was not provided");
119+
return Promise.reject(new Error("exp was not provided"));
120+
}
121+
if (payload.exp < upperNow) {
122+
Log.error("JoseUtil._validateJwt: exp is in the past", payload.exp);
123+
return Promise.reject(new Error("exp is in the past:" + payload.exp));
124+
}
123125
}
124126

125127
return Promise.resolve(payload);
126128
}
127129

128-
static _validateJwt(jwt, key, issuer, audience, clockSkew, now) {
130+
static _validateJwt(jwt, key, issuer, audience, clockSkew, now, timeInsensitive) {
129131

130-
return JoseUtil.validateJwtAttributes(jwt, issuer, audience, clockSkew, now).then(payload => {
132+
return JoseUtil.validateJwtAttributes(jwt, issuer, audience, clockSkew, now, timeInsensitive).then(payload => {
131133
try {
132134
if (!jws.JWS.verify(jwt, key, AllowedSigningAlgs)) {
133135
Log.error("JoseUtil._validateJwt: signature validation failed");

src/JsonService.js

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@ import { Log } from './Log';
55
import { Global } from './Global';
66

77
export class JsonService {
8-
constructor(additionalContentTypes = null, XMLHttpRequestCtor = Global.XMLHttpRequest) {
8+
constructor(
9+
additionalContentTypes = null,
10+
XMLHttpRequestCtor = Global.XMLHttpRequest,
11+
jwtHandler = null
12+
) {
913
if (additionalContentTypes && Array.isArray(additionalContentTypes))
1014
{
1115
this._contentTypes = additionalContentTypes.slice();
@@ -15,8 +19,12 @@ export class JsonService {
1519
this._contentTypes = [];
1620
}
1721
this._contentTypes.push('application/json');
22+
if (jwtHandler) {
23+
this._contentTypes.push('application/jwt');
24+
}
1825

1926
this._XMLHttpRequest = XMLHttpRequestCtor;
27+
this._jwtHandler = jwtHandler;
2028
}
2129

2230
getJson(url, token) {
@@ -33,6 +41,7 @@ export class JsonService {
3341
req.open('GET', url);
3442

3543
var allowedContentTypes = this._contentTypes;
44+
var jwtHandler = this._jwtHandler;
3645

3746
req.onload = function() {
3847
Log.debug("JsonService.getJson: HTTP response received, status", req.status);
@@ -48,6 +57,11 @@ export class JsonService {
4857
}
4958
});
5059

60+
if (found == "application/jwt") {
61+
jwtHandler(req).then(resolve, reject);
62+
return;
63+
}
64+
5165
if (found) {
5266
try {
5367
resolve(JSON.parse(req.responseText));

src/OidcClientSettings.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export class OidcClientSettings {
2525
// behavior flags
2626
filterProtocolClaims = true, loadUserInfo = true,
2727
staleStateAge = DefaultStaleStateAge, clockSkew = DefaultClockSkewInSeconds,
28+
userInfoJwtIssuer = 'OP',
2829
// other behavior
2930
stateStore = new WebStorageStateStore(),
3031
ResponseValidatorCtor = ResponseValidator,
@@ -57,6 +58,7 @@ export class OidcClientSettings {
5758
this._loadUserInfo = !!loadUserInfo;
5859
this._staleStateAge = staleStateAge;
5960
this._clockSkew = clockSkew;
61+
this._userInfoJwtIssuer = userInfoJwtIssuer;
6062

6163
this._stateStore = stateStore;
6264
this._validator = new ResponseValidatorCtor(this);
@@ -177,6 +179,9 @@ export class OidcClientSettings {
177179
get clockSkew() {
178180
return this._clockSkew;
179181
}
182+
get userInfoJwtIssuer() {
183+
return this._userInfoJwtIssuer;
184+
}
180185

181186
get stateStore() {
182187
return this._stateStore;

src/UserInfoService.js

Lines changed: 114 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,24 @@
44
import { JsonService } from './JsonService';
55
import { MetadataService } from './MetadataService';
66
import { Log } from './Log';
7+
import { JoseUtil } from './JoseUtil';
78

89
export class UserInfoService {
9-
constructor(settings, JsonServiceCtor = JsonService, MetadataServiceCtor = MetadataService) {
10+
constructor(
11+
settings,
12+
JsonServiceCtor = JsonService,
13+
MetadataServiceCtor = MetadataService,
14+
joseUtil = JoseUtil
15+
) {
1016
if (!settings) {
1117
Log.error("UserInfoService.ctor: No settings passed");
1218
throw new Error("settings");
1319
}
1420

1521
this._settings = settings;
16-
this._jsonService = new JsonServiceCtor();
22+
this._jsonService = new JsonServiceCtor(undefined, undefined, this._getClaimsFromJwt.bind(this));
1723
this._metadataService = new MetadataServiceCtor(this._settings);
24+
this._joseUtil = joseUtil;
1825
}
1926

2027
getClaims(token) {
@@ -32,4 +39,109 @@ export class UserInfoService {
3239
});
3340
});
3441
}
42+
43+
_getClaimsFromJwt(req) {
44+
try {
45+
let jwt = this._joseUtil.parseJwt(req.responseText);
46+
if (!jwt || !jwt.header || !jwt.payload) {
47+
Log.error("UserInfoService._getClaimsFromJwt: Failed to parse JWT", jwt);
48+
return Promise.reject(new Error("Failed to parse id_token"));
49+
}
50+
51+
var kid = jwt.header.kid;
52+
53+
let issuerPromise;
54+
switch (this._settings.userInfoJwtIssuer) {
55+
case 'OP':
56+
issuerPromise = this._metadataService.getIssuer();
57+
break;
58+
case 'ANY':
59+
issuerPromise = Promise.resolve(jwt.payload.iss);
60+
break;
61+
default:
62+
issuerPromise = Promise.resolve(this._settings.userInfoJwtIssuer);
63+
break;
64+
}
65+
66+
return issuerPromise.then(issuer => {
67+
Log.debug("UserInfoService._getClaimsFromJwt: Received issuer:" + issuer);
68+
69+
return this._metadataService.getSigningKeys().then(keys => {
70+
if (!keys) {
71+
Log.error("UserInfoService._getClaimsFromJwt: No signing keys from metadata");
72+
return Promise.reject(new Error("No signing keys from metadata"));
73+
}
74+
75+
Log.debug("UserInfoService._getClaimsFromJwt: Received signing keys");
76+
let key;
77+
if (!kid) {
78+
keys = this._filterByAlg(keys, jwt.header.alg);
79+
80+
if (keys.length > 1) {
81+
Log.error("UserInfoService._getClaimsFromJwt: No kid found in id_token and more than one key found in metadata");
82+
return Promise.reject(new Error("No kid found in id_token and more than one key found in metadata"));
83+
}
84+
else {
85+
// kid is mandatory only when there are multiple keys in the referenced JWK Set document
86+
// see http://openid.net/specs/openid-connect-core-1_0.html#Signing
87+
key = keys[0];
88+
}
89+
}
90+
else {
91+
key = keys.filter(key => {
92+
return key.kid === kid;
93+
})[0];
94+
}
95+
96+
if (!key) {
97+
Log.error("UserInfoService._getClaimsFromJwt: No key matching kid or alg found in signing keys");
98+
return Promise.reject(new Error("No key matching kid or alg found in signing keys"));
99+
}
100+
101+
let audience = this._settings.client_id;
102+
103+
let clockSkewInSeconds = this._settings.clockSkew;
104+
Log.debug("UserInfoService._getClaimsFromJwt: Validaing JWT; using clock skew (in seconds) of: ", clockSkewInSeconds);
105+
106+
return this._joseUtil.validateJwt(req.responseText, key, issuer, audience, clockSkewInSeconds, undefined, true).then(() => {
107+
Log.debug("UserInfoService._getClaimsFromJwt: JWT validation successful");
108+
return jwt.payload;
109+
});
110+
});
111+
});
112+
return;
113+
}
114+
catch (e) {
115+
Log.error("UserInfoService._getClaimsFromJwt: Error parsing JWT response", e.message);
116+
reject(e);
117+
return;
118+
}
119+
}
120+
121+
_filterByAlg(keys, alg) {
122+
var kty = null;
123+
if (alg.startsWith("RS")) {
124+
kty = "RSA";
125+
}
126+
else if (alg.startsWith("PS")) {
127+
kty = "PS";
128+
}
129+
else if (alg.startsWith("ES")) {
130+
kty = "EC";
131+
}
132+
else {
133+
Log.debug("UserInfoService._filterByAlg: alg not supported: ", alg);
134+
return [];
135+
}
136+
137+
Log.debug("UserInfoService._filterByAlg: Looking for keys that match kty: ", kty);
138+
139+
keys = keys.filter(key => {
140+
return key.kty === kty;
141+
});
142+
143+
Log.debug("UserInfoService._filterByAlg: Number of keys that match kty: ", kty, keys.length);
144+
145+
return keys;
146+
}
35147
}

0 commit comments

Comments
 (0)