Skip to content

Commit

Permalink
Add token.verify and deprecate idToken.verify
Browse files Browse the repository at this point in the history
Resolves: OKTA-99715
  • Loading branch information
lboyette-okta committed Oct 21, 2016
1 parent af21c3b commit ccd2b32
Show file tree
Hide file tree
Showing 12 changed files with 436 additions and 235 deletions.
5 changes: 3 additions & 2 deletions lib/clientBuilder.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ function OktaAuthBuilder(args) {
sdk.idToken = {
authorize: util.deprecateWrap('Use token.getWithoutPrompt, token.getWithPopup, or token.getWithRedirect ' +
'instead of idToken.authorize.', util.bind(token.getToken, null, sdk)),
verify: util.bind(token.verifyIdToken, null, sdk),
verify: util.deprecateWrap('Use token.verify instead of idToken.verify', util.bind(token.verifyIdToken, null, sdk)),
refresh: util.deprecateWrap('Use token.refresh instead of idToken.refresh',
util.bind(token.refreshIdToken, null, sdk)),
decode: util.deprecateWrap('Use token.decode instead of idToken.decode', token.decodeToken)
Expand All @@ -99,7 +99,8 @@ function OktaAuthBuilder(args) {
parseFromUrl: util.bind(token.parseFromUrl, null, sdk),
decode: token.decodeToken,
refresh: util.bind(token.refreshToken, null, sdk),
getUserInfo: util.bind(token.getUserInfo, null, sdk)
getUserInfo: util.bind(token.getUserInfo, null, sdk),
verify: util.bind(token.verifyToken, null, sdk)
};

// This is exposed so we can set window.location in our tests
Expand Down
52 changes: 48 additions & 4 deletions lib/oauthUtil.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
var http = require('./http');
var util = require('./util');
var AuthSdkError = require('./errors/AuthSdkError');
var config = require('./config');
var storageBuilder = require('./storageBuilder');

var httpCache = storageBuilder(localStorage, config.CACHE_STORAGE_NAME);

function isToken(obj) {
if (obj &&
Expand Down Expand Up @@ -43,18 +47,57 @@ function loadPopup(src, options) {
return window.open(src, title, appearance);
}

function getWellKnown(sdk) {
// TODO: Use the issuer when known (usually from the id_token)
return http.get(sdk, sdk.options.url + '/.well-known/openid-configuration', {
function getWellKnown(sdk, issuer) {
return http.get(sdk, (issuer || sdk.options.url) + '/.well-known/openid-configuration', {
cacheResponse: true
});
}

function validateClaims(sdk, claims, aud, iss) {
function getKey(sdk, issuer, kid) {
return getWellKnown(sdk, issuer)
.then(function(wellKnown) {
var jwksUri = wellKnown['jwks_uri'];

// Check our kid against the cached version (if it exists and isn't expired)
var cacheContents = httpCache.getStorage();
var cachedResponse = cacheContents[jwksUri];
if (cachedResponse && Date.now()/1000 < cachedResponse.expiresAt) {
var cachedKey = util.find(cachedResponse.response.keys, {
kid: kid
});

if (cachedKey) {
return cachedKey;
}
}

// Pull the latest keys if the key wasn't in the cache
return http.get(jwksUri, {
responseCacheDuration: config.DEFAULT_CACHE_DURATION
})
.then(function(keys) {
var key = util.find(keys, {
kid: kid
});

if (key) {
return key;
}

throw new AuthSdkError('The key id, ' + kid + ', was not found in the server\'s keys');
});
});
}

function validateClaims(sdk, claims, aud, iss, nonce) {
if (!claims || !iss || !aud) {
throw new AuthSdkError('The jwt, iss, and aud arguments are all required');
}

if (nonce && claims.nonce !== nonce) {
throw new AuthSdkError('OAuth flow response nonce doesn\'t match request nonce');
}

var now = Math.floor(new Date().getTime()/1000);

if (claims.iss !== iss) {
Expand Down Expand Up @@ -180,6 +223,7 @@ function hashToObject(hash) {

module.exports = {
getWellKnown: getWellKnown,
getKey: getKey,
validateClaims: validateClaims,
getOAuthUrls: getOAuthUrls,
loadFrame: loadFrame,
Expand Down
166 changes: 102 additions & 64 deletions lib/token.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,36 @@ function verifyIdToken(sdk, idToken, options) {
});
}

function verifyToken(sdk, token, nonce) {
return new Q()
.then(function() {
if (!token || !token.idToken) {
throw new AuthSdkError('Only idTokens may be verified');
}

var jwt = decodeToken(token.idToken);

// Standard claim validation
oauthUtil.validateClaims(sdk, jwt.payload, token.clientId, token.issuer, nonce);

// If the browser doesn't support native crypto, bail early
if (!sdk.features.isTokenVerifySupported()) {
return token;
}

return oauthUtil.getKey(sdk, token.issuer, jwt.header.kid)
.then(function(key) {
return sdkCrypto.verifyToken(token.idToken, key);
})
.then(function(valid) {
if (!valid) {
throw new AuthSdkError('The token signature is not valid');
}
return token;
});
});
}

function refreshIdToken(sdk, options) {
options = options || {};
options.display = null;
Expand Down Expand Up @@ -151,87 +181,94 @@ function addFragmentListener(sdk, windowEl, timeout) {
function handleOAuthResponse(sdk, oauthParams, res, urls) {
urls = urls || {};

if (res['error'] || res['error_description']) {
throw new OAuthError(res['error'], res['error_description']);
}

if (res.state !== oauthParams.state) {
throw new AuthSdkError('OAuth flow response state doesn\'t match request state');
}

var tokenTypes = oauthParams.responseType;
var scopes = util.clone(oauthParams.scopes);
var tokenDict = {};
var clientId = oauthParams.clientId || sdk.options.clientId;

if (res['id_token']) {
var jwt = sdk.token.decode(res['id_token']);
if (jwt.payload.nonce !== oauthParams.nonce) {
throw new AuthSdkError('OAuth flow response nonce doesn\'t match request nonce');
return new Q()
.then(function() {
if (res['error'] || res['error_description']) {
throw new OAuthError(res['error'], res['error_description']);
}

var clientId = oauthParams.clientId || sdk.options.clientId;
oauthUtil.validateClaims(sdk, jwt.payload, clientId, urls.issuer);

var idToken = {
idToken: res['id_token'],
claims: jwt.payload,
expiresAt: jwt.payload.exp,
scopes: scopes,
authorizeUrl: urls.authorizeUrl,
issuer: urls.issuer
};
if (res.state !== oauthParams.state) {
throw new AuthSdkError('OAuth flow response state doesn\'t match request state');
}

if (res['access_token']) {
var accessToken = {
accessToken: res['access_token'],
expiresAt: Number(res['expires_in']) + Math.floor(Date.now()/1000),
tokenType: res['token_type'],
scopes: scopes,
authorizeUrl: urls.authorizeUrl,
userinfoUrl: urls.userinfoUrl
};

if (Array.isArray(tokenTypes)) {
tokenDict['id_token'] = idToken;
} else {
return idToken;
if (Array.isArray(tokenTypes)) {
tokenDict['token'] = accessToken;
} else {
return accessToken;
}
}
}

if (res['access_token']) {
var accessToken = {
accessToken: res['access_token'],
expiresAt: Number(res['expires_in']) + Math.floor(Date.now()/1000),
tokenType: res['token_type'],
scopes: scopes,
authorizeUrl: urls.authorizeUrl,
userinfoUrl: urls.userinfoUrl
};

if (Array.isArray(tokenTypes)) {
tokenDict['token'] = accessToken;
} else {
return accessToken;
if (res['code']) {
var authorizationCode = {
authorizationCode: res['code']
};

if (Array.isArray(tokenTypes)) {
tokenDict['code'] = authorizationCode;
} else {
return authorizationCode;
}
}
}

if (res['code']) {
var authorizationCode = {
authorizationCode: res['code']
};
if (res['id_token']) {
var jwt = sdk.token.decode(res['id_token']);

var idToken = {
idToken: res['id_token'],
claims: jwt.payload,
expiresAt: jwt.payload.exp,
scopes: scopes,
authorizeUrl: urls.authorizeUrl,
issuer: urls.issuer,
clientId: clientId
};

if (Array.isArray(tokenTypes)) {
tokenDict['code'] = authorizationCode;
} else {
return authorizationCode;
return verifyToken(sdk, idToken, oauthParams.nonce)
.then(function(token) {
if (Array.isArray(tokenTypes)) {
tokenDict['id_token'] = idToken;
} else {
return idToken;
}
});
}
})
.then(function(codeOrToken) {
if (codeOrToken) {
return codeOrToken;
}
}

if (!tokenDict['token'] && !tokenDict['id_token']) {
throw new AuthSdkError('Unable to parse OAuth flow response');
}
if (!tokenDict['token'] && !tokenDict['id_token']) {
throw new AuthSdkError('Unable to parse OAuth flow response');
}

var tokens = [];
var tokens = [];

// Create token array in the order of the responseType array
for (var t = 0, tl = tokenTypes.length; t < tl; t++) {
var tokenType = tokenTypes[t];
if (tokenDict[tokenType]) {
tokens.push(tokenDict[tokenType]);
// Create token array in the order of the responseType array
for (var t = 0, tl = tokenTypes.length; t < tl; t++) {
var tokenType = tokenTypes[t];
if (tokenDict[tokenType]) {
tokens.push(tokenDict[tokenType]);
}
}
}

return tokens;
return tokens;
});
}

function getDefaultOAuthParams(sdk, oauthOptions) {
Expand Down Expand Up @@ -619,5 +656,6 @@ module.exports = {
decodeToken: decodeToken,
verifyIdToken: verifyIdToken,
refreshToken: refreshToken,
getUserInfo: getUserInfo
getUserInfo: getUserInfo,
verifyToken: verifyToken
};
22 changes: 11 additions & 11 deletions test/spec/general.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ define(function(require) {

setupVerifyIdTokenTest({
title: 'verifies a valid idToken',
idToken: tokens.verifiableIdToken,
idToken: tokens.standardIdToken,
expectations: function (test, res) {
expect(res).toEqual(true);
}
Expand Down Expand Up @@ -120,7 +120,7 @@ define(function(require) {

setupVerifyIdTokenTest({
title: 'rejects an invalid idToken due to expiration',
idToken: tokens.verifiableIdToken,
idToken: tokens.standardIdToken,
execute: function(test, opts) {
util.warpToDistantPast();
return test.oa.idToken.verify(opts.idToken, opts.verifyOpts)
Expand All @@ -137,7 +137,7 @@ define(function(require) {
setupVerifyIdTokenTest({
title: 'verifies an idToken that would be invalid, except ' +
'we\'re using the expirationTime option',
idToken: tokens.verifiableIdToken,
idToken: tokens.standardIdToken,
verifyOpts: {
expirationTime: 9999999999
},
Expand All @@ -156,7 +156,7 @@ define(function(require) {

setupVerifyIdTokenTest({
title: 'verifies a valid idToken using single audience option',
idToken: tokens.verifiableIdToken,
idToken: tokens.standardIdToken,
verifyOpts: {
audience: 'NPSfOkH5eZrTy8PMDlvx'
},
Expand All @@ -167,7 +167,7 @@ define(function(require) {

setupVerifyIdTokenTest({
title: 'rejects an invalid idToken using single audience option',
idToken: tokens.verifiableIdToken,
idToken: tokens.standardIdToken,
verifyOpts: {
audience: 'invalid'
},
Expand All @@ -178,7 +178,7 @@ define(function(require) {

setupVerifyIdTokenTest({
title: 'verifies a valid idToken using multiple audience option (all valid)',
idToken: tokens.verifiableIdToken,
idToken: tokens.standardIdToken,
verifyOpts: {
audience: ['NPSfOkH5eZrTy8PMDlvx', 'NPSfOkH5eZrTy8PMDlvx']
},
Expand All @@ -189,7 +189,7 @@ define(function(require) {

setupVerifyIdTokenTest({
title: 'verifies a valid idToken using multiple audience option (valid and invalid)',
idToken: tokens.verifiableIdToken,
idToken: tokens.standardIdToken,
verifyOpts: {
audience: ['NPSfOkH5eZrTy8PMDlvx', 'invalid2']
},
Expand All @@ -200,7 +200,7 @@ define(function(require) {

setupVerifyIdTokenTest({
title: 'rejects an invalid idToken using multiple audience option (all invalid)',
idToken: tokens.verifiableIdToken,
idToken: tokens.standardIdToken,
verifyOpts: {
audience: ['invalid1', 'invalid2']
},
Expand All @@ -211,9 +211,9 @@ define(function(require) {

setupVerifyIdTokenTest({
title: 'verifies a valid idToken using issuer option',
idToken: tokens.verifiableIdToken,
idToken: tokens.standardIdToken,
verifyOpts: {
issuer: 'https://lboyette.trexcloud.com'
issuer: 'https://auth-js-test.okta.com'
},
expectations: function (test, res) {
expect(res).toEqual(true);
Expand All @@ -222,7 +222,7 @@ define(function(require) {

setupVerifyIdTokenTest({
title: 'rejects an invalid idToken using issuer option',
idToken: tokens.verifiableIdToken,
idToken: tokens.standardIdToken,
verifyOpts: {
issuer: 'invalid'
},
Expand Down
Loading

0 comments on commit ccd2b32

Please sign in to comment.