Skip to content
This repository has been archived by the owner on Mar 3, 2022. It is now read-only.

Commit

Permalink
Merge pull request #636 from asleire/dev
Browse files Browse the repository at this point in the history
Add UserInfo JWT response support
  • Loading branch information
brockallen authored Feb 10, 2019
2 parents ad4d153 + b42ae57 commit 5bbc961
Show file tree
Hide file tree
Showing 5 changed files with 163 additions and 29 deletions.
1 change: 1 addition & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ export interface OidcClientSettings {
readonly staleStateAge?: number;
readonly clockSkew?: number;
readonly stateStore?: StateStore;
readonly userInfoJwtIssuer?: 'ANY' | 'OP' | string;
ResponseValidatorCtor?: ResponseValidatorCtor;
MetadataServiceCtor?: MetadataServiceCtor;
extraQueryParams?: {};
Expand Down
54 changes: 28 additions & 26 deletions src/JoseUtil.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export class JoseUtil {
}
}

static validateJwt(jwt, key, issuer, audience, clockSkew, now) {
static validateJwt(jwt, key, issuer, audience, clockSkew, now, timeInsensitive) {
Log.debug("JoseUtil.validateJwt");

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

return JoseUtil._validateJwt(jwt, key, issuer, audience, clockSkew, now);
return JoseUtil._validateJwt(jwt, key, issuer, audience, clockSkew, now, timeInsensitive);
}
catch (e) {
Log.error(e && e.message || e);
return Promise.reject("JWT validation failed");
}
}

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

var lowerNow = now + clockSkew;
var upperNow = now - clockSkew;
if (!timeInsensitive) {
var lowerNow = now + clockSkew;
var upperNow = now - clockSkew;

if (!payload.iat) {
Log.error("JoseUtil._validateJwt: iat was not provided");
return Promise.reject(new Error("iat was not provided"));
}
if (lowerNow < payload.iat) {
Log.error("JoseUtil._validateJwt: iat is in the future", payload.iat);
return Promise.reject(new Error("iat is in the future: " + payload.iat));
}
if (!payload.iat) {
Log.error("JoseUtil._validateJwt: iat was not provided");
return Promise.reject(new Error("iat was not provided"));
}
if (lowerNow < payload.iat) {
Log.error("JoseUtil._validateJwt: iat is in the future", payload.iat);
return Promise.reject(new Error("iat is in the future: " + payload.iat));
}

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

if (!payload.exp) {
Log.error("JoseUtil._validateJwt: exp was not provided");
return Promise.reject(new Error("exp was not provided"));
}
if (payload.exp < upperNow) {
Log.error("JoseUtil._validateJwt: exp is in the past", payload.exp);
return Promise.reject(new Error("exp is in the past:" + payload.exp));
if (!payload.exp) {
Log.error("JoseUtil._validateJwt: exp was not provided");
return Promise.reject(new Error("exp was not provided"));
}
if (payload.exp < upperNow) {
Log.error("JoseUtil._validateJwt: exp is in the past", payload.exp);
return Promise.reject(new Error("exp is in the past:" + payload.exp));
}
}

return Promise.resolve(payload);
}

static _validateJwt(jwt, key, issuer, audience, clockSkew, now) {
static _validateJwt(jwt, key, issuer, audience, clockSkew, now, timeInsensitive) {

return JoseUtil.validateJwtAttributes(jwt, issuer, audience, clockSkew, now).then(payload => {
return JoseUtil.validateJwtAttributes(jwt, issuer, audience, clockSkew, now, timeInsensitive).then(payload => {
try {
if (!jws.JWS.verify(jwt, key, AllowedSigningAlgs)) {
Log.error("JoseUtil._validateJwt: signature validation failed");
Expand Down
16 changes: 15 additions & 1 deletion src/JsonService.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ import { Log } from './Log';
import { Global } from './Global';

export class JsonService {
constructor(additionalContentTypes = null, XMLHttpRequestCtor = Global.XMLHttpRequest) {
constructor(
additionalContentTypes = null,
XMLHttpRequestCtor = Global.XMLHttpRequest,
jwtHandler = null
) {
if (additionalContentTypes && Array.isArray(additionalContentTypes))
{
this._contentTypes = additionalContentTypes.slice();
Expand All @@ -15,8 +19,12 @@ export class JsonService {
this._contentTypes = [];
}
this._contentTypes.push('application/json');
if (jwtHandler) {
this._contentTypes.push('application/jwt');
}

this._XMLHttpRequest = XMLHttpRequestCtor;
this._jwtHandler = jwtHandler;
}

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

var allowedContentTypes = this._contentTypes;
var jwtHandler = this._jwtHandler;

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

if (found == "application/jwt") {
jwtHandler(req).then(resolve, reject);
return;
}

if (found) {
try {
resolve(JSON.parse(req.responseText));
Expand Down
5 changes: 5 additions & 0 deletions src/OidcClientSettings.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export class OidcClientSettings {
// behavior flags
filterProtocolClaims = true, loadUserInfo = true,
staleStateAge = DefaultStaleStateAge, clockSkew = DefaultClockSkewInSeconds,
userInfoJwtIssuer = 'OP',
// other behavior
stateStore = new WebStorageStateStore(),
ResponseValidatorCtor = ResponseValidator,
Expand Down Expand Up @@ -57,6 +58,7 @@ export class OidcClientSettings {
this._loadUserInfo = !!loadUserInfo;
this._staleStateAge = staleStateAge;
this._clockSkew = clockSkew;
this._userInfoJwtIssuer = userInfoJwtIssuer;

this._stateStore = stateStore;
this._validator = new ResponseValidatorCtor(this);
Expand Down Expand Up @@ -177,6 +179,9 @@ export class OidcClientSettings {
get clockSkew() {
return this._clockSkew;
}
get userInfoJwtIssuer() {
return this._userInfoJwtIssuer;
}

get stateStore() {
return this._stateStore;
Expand Down
116 changes: 114 additions & 2 deletions src/UserInfoService.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,24 @@
import { JsonService } from './JsonService';
import { MetadataService } from './MetadataService';
import { Log } from './Log';
import { JoseUtil } from './JoseUtil';

export class UserInfoService {
constructor(settings, JsonServiceCtor = JsonService, MetadataServiceCtor = MetadataService) {
constructor(
settings,
JsonServiceCtor = JsonService,
MetadataServiceCtor = MetadataService,
joseUtil = JoseUtil
) {
if (!settings) {
Log.error("UserInfoService.ctor: No settings passed");
throw new Error("settings");
}

this._settings = settings;
this._jsonService = new JsonServiceCtor();
this._jsonService = new JsonServiceCtor(undefined, undefined, this._getClaimsFromJwt.bind(this));
this._metadataService = new MetadataServiceCtor(this._settings);
this._joseUtil = joseUtil;
}

getClaims(token) {
Expand All @@ -32,4 +39,109 @@ export class UserInfoService {
});
});
}

_getClaimsFromJwt(req) {
try {
let jwt = this._joseUtil.parseJwt(req.responseText);
if (!jwt || !jwt.header || !jwt.payload) {
Log.error("UserInfoService._getClaimsFromJwt: Failed to parse JWT", jwt);
return Promise.reject(new Error("Failed to parse id_token"));
}

var kid = jwt.header.kid;

let issuerPromise;
switch (this._settings.userInfoJwtIssuer) {
case 'OP':
issuerPromise = this._metadataService.getIssuer();
break;
case 'ANY':
issuerPromise = Promise.resolve(jwt.payload.iss);
break;
default:
issuerPromise = Promise.resolve(this._settings.userInfoJwtIssuer);
break;
}

return issuerPromise.then(issuer => {
Log.debug("UserInfoService._getClaimsFromJwt: Received issuer:" + issuer);

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

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

if (keys.length > 1) {
Log.error("UserInfoService._getClaimsFromJwt: 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];
}

if (!key) {
Log.error("UserInfoService._getClaimsFromJwt: 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"));
}

let audience = this._settings.client_id;

let clockSkewInSeconds = this._settings.clockSkew;
Log.debug("UserInfoService._getClaimsFromJwt: Validaing JWT; using clock skew (in seconds) of: ", clockSkewInSeconds);

return this._joseUtil.validateJwt(req.responseText, key, issuer, audience, clockSkewInSeconds, undefined, true).then(() => {
Log.debug("UserInfoService._getClaimsFromJwt: JWT validation successful");
return jwt.payload;
});
});
});
return;
}
catch (e) {
Log.error("UserInfoService._getClaimsFromJwt: Error parsing JWT response", e.message);
reject(e);
return;
}
}

_filterByAlg(keys, alg) {
var kty = null;
if (alg.startsWith("RS")) {
kty = "RSA";
}
else if (alg.startsWith("PS")) {
kty = "PS";
}
else if (alg.startsWith("ES")) {
kty = "EC";
}
else {
Log.debug("UserInfoService._filterByAlg: alg not supported: ", alg);
return [];
}

Log.debug("UserInfoService._filterByAlg: Looking for keys that match kty: ", kty);

keys = keys.filter(key => {
return key.kty === kty;
});

Log.debug("UserInfoService._filterByAlg: Number of keys that match kty: ", kty, keys.length);

return keys;
}
}

0 comments on commit 5bbc961

Please sign in to comment.