Skip to content

Commit

Permalink
Merge pull request #278 from auth0/jwt-verification
Browse files Browse the repository at this point in the history
Add token validation and signature verification to the parseHash method
  • Loading branch information
hzalaz authored Jan 3, 2017
2 parents 3085e77 + 1239aff commit c9c5bd4
Show file tree
Hide file tree
Showing 13 changed files with 322 additions and 211 deletions.
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,15 @@ auth0.login({
- **parseHash()**: Parses the url hash in order to extract the token

```js
var authResult = auth0.parseHash();
if (!authResult.error) {
auth0.parseHash(function(err, authResult) {
if (err) {
return console.log(err);
}

auth0.client.userInfo(authResult.accessToken, function(err, user) {
...
});
}
});
```

- **renewAuth(options, cb)**: Gets a new token from Auth0 (the user should be authenticated using the hosted login page first)
Expand Down
7 changes: 3 additions & 4 deletions example/callback.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,9 @@
clientID: '3GGMIEuBPZ28lb6NBDNARaEZisqFakAs',
responseType: 'token'
});
var result = auth0.parseHash(window.location.hash);
if (result) {
parent.postMessage(result, "http://localhost:3000/"); //The second parameter should be your domain
}
var result = auth0.parseHash(window.location.hash, function(err, data) {
parent.postMessage(err || data, "http://localhost:3000/");
});
</script>
</head>
<body></body>
Expand Down
16 changes: 10 additions & 6 deletions example/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -170,15 +170,19 @@ <h2>Console:</h2>
domain: 'auth0-tests-auth0js.auth0.com',
redirectUri: 'http://localhost:3000/example',
clientID: '3GGMIEuBPZ28lb6NBDNARaEZisqFakAs',
audience: 'https://auth0-tests-auth0js.auth0.com/userinfo',
responseType: 'token'
});

var hash = webAuth.parseHash();
if (hash) {
htmlConsole.dumpCallback(hash.error ? hash : null, hash.error ? null : hash);
webAuth.parseHash(function(err, data) {
if (err) {
return htmlConsole.dumpCallback(err);
}

htmlConsole.dumpCallback(null, data);
window.location.hash = '';
webAuth.client.userInfo(hash.accessToken, htmlConsole.dumpCallback.bind(htmlConsole));
}
webAuth.client.userInfo(data.accessToken, htmlConsole.dumpCallback.bind(htmlConsole));
});

$('#clear-console').click(function () {
$('#clear-console').removeClass('icon-budicon-498');
Expand Down Expand Up @@ -284,7 +288,7 @@ <h2>Console:</h2>
e.preventDefault();
webAuth.renewAuth({
usePostMessage: true,
scope: 'openid',
scope:'',
audience: 'https://auth0-tests-auth0js.auth0.com/userinfo',
redirectURI: 'http://localhost:3000/example/callback.html'
},
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@
"author": "Auth0",
"license": "MIT",
"dependencies": {
"Base64": "~0.1.3",
"base64-js": "^1.2.0",
"idtoken-verifier": "^1.0.0",
"superagent": "^2.3.0",
"url-join": "^1.1.0",
"winchan": "^0.1.4"
Expand All @@ -45,7 +46,7 @@
"gulp-util": "^3.0.7",
"istanbul": "^0.4.5",
"jsdoc-to-markdown": "^2.0.1",
"mocha": "^3.1.2",
"mocha": "^3.2.0",
"semver": "^5.3.0",
"sinon": "^1.17.6",
"smart-banner-webpack-plugin": "^2.0.0",
Expand Down
42 changes: 32 additions & 10 deletions src/helper/base64_url.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,44 @@
var Base64 = require('Base64');
var base64 = require('base64-js');

function padding(str) {
var mod = (str.length % 4);
var pad = 4 - mod;

if (mod === 0) {
return str;
}

return str + (new Array(1 + pad)).join('=');
}

function stringToByteArray(str) {
var arr = new Array(str.length);
for (var a = 0; a < str.length; a++) {
arr[a] = str.charCodeAt(a);
}
return arr;
}

function byteArrayToString(array) {
var result = "";
for (var i = 0; i < array.length; i++) {
result += String.fromCharCode(array[i]);
}
return result;
}

function encode(str) {
return Base64.btoa(str)
return base64.fromByteArray(stringToByteArray(str))
.replace(/\+/g, '-') // Convert '+' to '-'
.replace(/\//g, '_') // Convert '/' to '_'
.replace(/=+$/, ''); // Remove ending '='
.replace(/\//g, '_'); // Convert '/' to '_'
}


function decode(str) {
// Add removed at end '='
str += Array(5 - str.length % 4).join('=');

str = str
str = padding(str)
.replace(/\-/g, '+') // Convert '-' to '+'
.replace(/_/g, '/'); // Convert '_' to '/'

return Base64.atob(str);
return byteArrayToString(base64.toByteArray(str));
}

module.exports = {
Expand Down
20 changes: 12 additions & 8 deletions src/helper/iframe-handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,16 @@ IframeHandler.prototype.messageEventListener = function (e) {
};

IframeHandler.prototype.loadEventListener = function () {
var result = this.auth0.parseHash(this.iframe.contentWindow.location.hash);
if (!result) {
return;
}

this.destroy();
this.callbackHandler(result);
var _this = this;
this.auth0.parseHash(
{ hash: this.iframe.contentWindow.location.hash },
function (error, result) {
if (error || result) {
_this.destroy();
_this.callback(error, result);
}
}
);
};

IframeHandler.prototype.callbackHandler = function (result) {
Expand All @@ -80,16 +83,17 @@ IframeHandler.prototype.timeoutHandler = function () {

IframeHandler.prototype.destroy = function () {
var _this = this;
var _window = windowHelper.getWindow();

clearTimeout(this.timeoutHandle);

this._destroyTimeout = setTimeout(function () {
var _window = windowHelper.getWindow();
if (_this.usePostMessage) {
_window.removeEventListener('message', _this.transientMessageEventListener, false);
} else {
_this.iframe.removeEventListener('load', _this.transientEventListener, false);
}

_window.document.body.removeChild(_this.iframe);
}, 0);
};
Expand Down
125 changes: 75 additions & 50 deletions src/web-auth/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
var IdTokenVerifier = require('idtoken-verifier');

var assert = require('../helper/assert');
var error = require('../helper/error');
var jwt = require('../helper/jwt');
Expand Down Expand Up @@ -32,19 +34,29 @@ function WebAuth(options) {
redirectUri: { optional: true, type: 'string', message: 'redirectUri is not valid' },
scope: { optional: true, type: 'string', message: 'audience is not valid' },
audience: { optional: true, type: 'string', message: 'scope is not valid' },
tenant: { optional: true, type: 'string', message: 'tenant option is not valid. Required when using custom domains.' },
_disableDeprecationWarnings: { optional: true, type: 'boolean', message: '_disableDeprecationWarnings option is not valid' },
_sendTelemetry: { optional: true, type: 'boolean', message: '_sendTelemetry option is not valid' },
_telemetryInfo: { optional: true, type: 'object', message: '_telemetryInfo option is not valid' }
});

if (options.overrides) {
assert.check(options.overrides, { type: 'object', message: 'overrides option is not valid' }, {
__tenant: { type: 'string', message: '__tenant option is required' },
__token_issuer: { type: 'string', message: '__token_issuer option is required' }
});
}
/* eslint-enable */

this.baseOptions = options;

this.baseOptions._sendTelemetry = this.baseOptions._sendTelemetry === false ?
this.baseOptions._sendTelemetry : true;

this.baseOptions.tenant = this.baseOptions.domain.split('.')[0];
this.baseOptions.tenant = (this.overrides && this.overrides.__tenant)
|| this.baseOptions.domain.split('.')[0];

this.baseOptions.token_issuer = (this.overrides && this.overrides.__token_issuer)
|| 'https://' + this.baseOptions.domain + '/';

this.transactionManager = new TransactionManager(this.baseOptions.transaction);

Expand All @@ -57,21 +69,29 @@ function WebAuth(options) {
* Parse the url hash and extract the access token or id token depending on the transaction.
*
* @method parseHash
* @param {String} hash: the url hash or null to automatically extract from window.location.hash
* @param {Object} options: state and nonce can be provided to verify the response
* @param {Object} options:
* @param {String} options.state [OPTIONAL] to verify the response
* @param {String} options.nonce [OPTIONAL] to verify the id_token
* @param {String} options.hash [OPTIONAL] the url hash. If not provided it will extract from window.location.hash
* @param {Function} cb: function(err, token_payload)
*/
WebAuth.prototype.parseHash = function (hash, options) {
WebAuth.prototype.parseHash = function (options, cb) {
var parsedQs;
var err;
var token;

if (!cb && typeof options === 'function') {
cb = options;
options = {};
} else {
options = options || {};
}

var _window = windowHelper.getWindow();

var hashStr = hash || _window.location.hash;
var hashStr = options.hash === undefined ? _window.location.hash : options.hash;
hashStr = hashStr.replace(/^#?\/?/, '');

options = options || {};

parsedQs = qs.parse(hashStr);

if (parsedQs.hasOwnProperty('error')) {
Expand All @@ -81,33 +101,40 @@ WebAuth.prototype.parseHash = function (hash, options) {
err.state = parsedQs.state;
}

return err;
return cb(err);
}

if (!parsedQs.hasOwnProperty('access_token')
&& !parsedQs.hasOwnProperty('id_token')
&& !parsedQs.hasOwnProperty('refresh_token')) {
return null;
return cb(null, null);
}

if (parsedQs.id_token) {
token = this.validateToken(parsedQs.id_token, parsedQs.state || options.state, options.nonce);
if (token.error) {
return token;
}
this.validateToken(parsedQs.id_token, parsedQs.state || options.state, options.nonce, function (err, response) {
if (err) {
return cb(err);
}

return cb(null, buildParseHashResponse(parsedQs, response));
});
} else {
cb(null, buildParseHashResponse(parsedQs, null));
}
};

function buildParseHashResponse(qs, token) {
return {
accessToken: parsedQs.access_token || null,
idToken: parsedQs.id_token || null,
accessToken: qs.access_token || null,
idToken: qs.id_token || null,
idTokenPayload: token && token.payload ? token.payload : null,
appStatus: token ? token.appStatus || null : null,
refreshToken: parsedQs.refresh_token || null,
state: parsedQs.state || null,
expiresIn: parsedQs.expires_in || null,
tokenType: parsedQs.token_type || null
refreshToken: qs.refresh_token || null,
state: qs.state || null,
expiresIn: qs.expires_in || null,
tokenType: qs.token_type || null
};
};
}

/**
* Decodes the id_token and verifies the nonce.
Expand All @@ -116,40 +143,33 @@ WebAuth.prototype.parseHash = function (hash, options) {
* @param {String} token
* @param {String} state
* @param {String} nonce
* @param {Function} cb: function(err, {payload, transaction})
*/
WebAuth.prototype.validateToken = function (token, state, nonce) {
WebAuth.prototype.validateToken = function (token, state, nonce, cb) {
var audiences;
var transaction;
var transactionNonce;
var tokenNonce;
var prof = jwt.getPayload(token);

audiences = assert.isArray(prof.aud) ? prof.aud : [prof.aud];
if (audiences.indexOf(this.baseOptions.clientID) === -1) {
return error.invalidJwt(
'The clientID configured (' + this.baseOptions.clientID + ') does not match ' +
'with the clientID set in the token (' + audiences.join(', ') + ').');
}

transaction = this.transactionManager.getStoredTransaction(state);
transactionNonce = (transaction && transaction.nonce) || nonce;
tokenNonce = prof.nonce || null;
transactionNonce = (transaction && transaction.nonce) || nonce || null;

if (transactionNonce && tokenNonce && transactionNonce !== tokenNonce) {
return error.invalidJwt('Nonce does not match');
}
var verifier = new IdTokenVerifier({
issuer: this.baseOptions.token_issuer,
audience: this.baseOptions.clientID,
__disableExpirationCheck: this.baseOptions.__disableExpirationCheck
});

// iss should be the Auth0 domain (i.e.: https://contoso.auth0.com/)
if (prof.iss && prof.iss !== 'https://' + this.baseOptions.domain + '/') {
return error.invalidJwt(
'The domain configured (https://' + this.baseOptions.domain + '/) does not match ' +
'with the domain set in the token (' + prof.iss + ').');
}
verifier.verify(token, transactionNonce, function (err, payload) {
if (err) {
return cb(error.invalidJwt(err.message));
}

return {
payload: prof,
transaction: transaction
};
cb(null, {
payload: payload,
transaction: transaction
});
});
};

/**
Expand Down Expand Up @@ -189,17 +209,22 @@ WebAuth.prototype.renewAuth = function (options, cb) {
params = objectHelper.blacklist(params, ['usePostMessage', 'tenant']);

handler = new SilentAuthenticationHandler(this, this.client.buildAuthorizeUrl(params));

handler.login(usePostMessage, function (err, data) {
if (err) {
return cb(err);
}

if (data.id_token) {
prof = _this.validateToken(data.id_token, options.state);
if (prof.error) {
cb(prof);
}
data.idTokenPayload = prof;
return _this.validateToken(data.id_token, options.state, options.nonce, function (err, payload) {
if (err) {
return cb(err);
}

data.idTokenPayload = payload;

return cb(null, data);
});
}

return cb(err, data);
Expand Down
Loading

0 comments on commit c9c5bd4

Please sign in to comment.