Skip to content

Commit

Permalink
Merge pull request #18 from auth0-extensions/Add-samesite-support
Browse files Browse the repository at this point in the history
Add samesite support
  • Loading branch information
faroceann committed Jan 8, 2020
2 parents e8c0c55 + 6fd2bf8 commit 9f6c619
Show file tree
Hide file tree
Showing 4 changed files with 428 additions and 134 deletions.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
{
"name": "auth0-extension-express-tools",
"version": "1.1.9",
"version": "2.0.0",
"description": "A set of tools and utilities to simplify the development of Auth0 Extensions with Express.",
"main": "src/index.js",
"dependencies": {
"auth0-extension-tools": "^1.3.0",
"cookie-parser": "^1.4.3",
"express": "^4.12.4",
"express": "^4.17.1",
"express-conditional-middleware": "2.0.0",
"express-jwt": "^3.1.0",
"jwks-rsa": "^1.1.1",
Expand Down
26 changes: 19 additions & 7 deletions src/routes/dashboardAdmins.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,19 @@ module.exports = function(options) {
router.get(urlPrefix + '/login', function(req, res) {
const basePath = urlHelpers.getBasePath(req);
const state = crypto.randomBytes(16).toString('hex');
const nonce = crypto.randomBytes(16).toString('hex');
res.cookie(stateKey, state, { path: basePath });
res.cookie(nonceKey, nonce, { path: basePath });

const nonce = crypto.randomBytes(16).toString('hex');
const basicAttr = {
httpOnly: true,
path: basePath
};

res.cookie(stateKey, state, Object.assign({}, basicAttr, { sameSite: 'None', secure: true }));
res.cookie(nonceKey, nonce, Object.assign({}, basicAttr, { sameSite: 'None', secure: true }));

// create legacy cookie
res.cookie(stateKey + '_compat', state, basicAttr);
res.cookie(nonceKey + '_compat', nonce, basicAttr);

const redirectTo = sessionManager.createAuthorizeUrl({
redirectUri: urlHelpers.getBaseUrl(req) + urlPrefix + '/login/callback',
scopes: options.scopes,
Expand All @@ -101,11 +110,10 @@ module.exports = function(options) {
return next(new tools.ValidationError('Login failed. Invalid token.'));
}

if (!req.cookies || req.cookies[nonceKey] !== decoded.nonce) {
if ((req.cookies && req.cookies[nonceKey] && req.cookies[nonceKey] !== decoded.nonce) || (req.cookies && req.cookies[nonceKey + '_compat'] && req.cookies[nonceKey + '_compat'] !== decoded.nonce)) {
return next(new tools.ValidationError('Login failed. Nonce mismatch.'));
}

if (!req.cookies || req.cookies[stateKey] !== req.body.state) {
if ((req.cookies && req.cookies[stateKey] && req.cookies[stateKey] !== req.body.state) || (req.cookies && req.cookies[stateKey + '_compat'] && req.cookies[stateKey + '_compat'] !== req.body.state)) {
return next(new tools.ValidationError('Login failed. State mismatch.'));
}

Expand All @@ -121,6 +129,8 @@ module.exports = function(options) {
.then(function(token) {
res.clearCookie(stateKey, { path: basePath });
res.clearCookie(nonceKey, { path: basePath });
res.clearCookie(stateKey + '_compat', { path: basePath });
res.clearCookie(nonceKey + '_compat', { path: basePath });
res.header('Content-Type', 'text/html');
res.status(200).send('<html>' +
'<head>' +
Expand All @@ -141,6 +151,8 @@ module.exports = function(options) {
const encodedBaseUrl = encodeURIComponent(urlHelpers.getBaseUrl(req));
res.clearCookie(stateKey, { path: basePath });
res.clearCookie(nonceKey, { path: basePath });
res.clearCookie(stateKey + '_compat', { path: basePath });
res.clearCookie(nonceKey + '_compat', { path: basePath });
res.header('Content-Type', 'text/html');
res.status(200).send(
'<html>' +
Expand Down
165 changes: 161 additions & 4 deletions tests/routes/dashboardAdmins.js
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,15 @@ tape('dashboardAdmins should redirect to auth0 on /login', function(t) {
const res = {
cookie: function(key, value, options) {
cookies[key] = value;
t.equal(options.httpOnly, true);
t.equal(options.path, '/login/');
if (!key.includes('_compat')) {
t.equal(options.sameSite, 'None');
t.equal(options.secure, true);
} else {
t.false(options.sameSite);
t.false(options.secure);
}
},
redirect: function(url) {
const expectedUrl =
Expand All @@ -188,8 +196,8 @@ tape('dashboardAdmins should redirect to auth0 on /login', function(t) {
'&scope=openid%20name%20email' +
'&expiration=36000' +
'&redirect_uri=https%3A%2Flogin%2Flogin%2Fcallback&audience=https%3A%2F%2Ftest.auth0.com%2Fapi%2Fv2%2F' +
'&nonce=' + cookies.nonce +
'&state=' + cookies.state;
'&nonce=' + (cookies.nonce || cookies['nonce_compat']) +
'&state=' + (cookies.state || cookies['state_compat']);
t.ok(url);
t.equal(url, expectedUrl);
t.end();
Expand Down Expand Up @@ -241,6 +249,49 @@ tape('dashboardAdmins should return ValidationError in case of nonce mismatch',
mw(req, {}, next);
});

tape('dashboardAdmins should return ValidationError in case of legacy nonce mismatch', function(t) {
const mw = dashboardAdmins({
secret: 'abc',
audience: 'urn:api',
rta: 'test.auth0.com',
domain: 'test.auth0.com',
baseUrl: 'https://test.auth0.com/api/v2/',
clientName: 'Some Client'
});

const token = tokens.sign(certs.bar.private, 'key2', {
iss: 'https://test.auth0.com/',
sub: '1234567890',
aud: 'https://test.auth0.com/api/v2/',
name: 'John Doe',
admin: true,
nonce_compat: 'nonce'
});

const req = {
headers: {},
cookies: {
state: 'state',
nonce_compat: 'another_nonce'
},
body: {
state: 'state',
id_token: token,
access_token: token
},
url: 'http://api/login/callback',
method: 'post'
};

const next = function(err) {
t.ok(err);
t.equal(err.name, 'ValidationError');
t.end();
};

mw(req, {}, next);
});

tape('dashboardAdmins should return ValidationError in case of state mismatch', function(t) {
const mw = dashboardAdmins({
secret: 'abc',
Expand Down Expand Up @@ -284,6 +335,50 @@ tape('dashboardAdmins should return ValidationError in case of state mismatch',
mw(req, {}, next);
});

tape('dashboardAdmins should return ValidationError in case of legacy state mismatch', function(t) {
const mw = dashboardAdmins({
secret: 'abc',
audience: 'urn:api',
rta: 'test.auth0.com',
domain: 'test.auth0.com',
baseUrl: 'https://test.auth0.com/api/v2/',
clientName: 'Some Client'
});

const token = tokens.sign(certs.bar.private, 'key2', {
iss: 'https://test.auth0.com/',
sub: '1234567890',
aud: 'https://test.auth0.com/api/v2/',
name: 'John Doe',
admin: true,
state_compat: 'state',
nonce: 'nonce'
});

const req = {
headers: {},
cookies: {
state_compat: 'another_state',
nonce: 'nonce'
},
body: {
state: 'state',
id_token: token,
access_token: token
},
url: 'http://api/login/callback',
method: 'post'
};

const next = function(err) {
t.ok(err);
t.equal(err.name, 'ValidationError');
t.end();
};

mw(req, {}, next);
});

tape('dashboardAdmins should return 200 if everything is ok', function(t) {
const mw = dashboardAdmins({
secret: 'abc',
Expand Down Expand Up @@ -324,7 +419,67 @@ tape('dashboardAdmins should return 200 if everything is ok', function(t) {
header: function() {},
clearCookie: function(name) {
if (name === 'nonce') t.equal(name, 'nonce');
else t.equal(name, 'state');
else if (name === 'nonce_compat') t.equal(name, 'nonce_compat');
else if (name === 'state') t.equal(name, 'state');
else t.equal(name, 'state_compat');
},
status: function(status) {
return {
send: function(html) {
t.ok(html);
t.equal(status, 200);
t.end();
}
};
}
};

mw(req, res);
});

tape('dashboardAdmins should return 200 with legacy nonce and state', function(t) {
const mw = dashboardAdmins({
secret: 'abc',
audience: 'urn:api',
rta: 'test.auth0.com',
domain: 'test.auth0.com',
baseUrl: 'https://test.auth0.com/api/v2/',
clientName: 'Some Client'
});

tokens.wellKnownEndpoint('test.auth0.com', certs.bar.cert, 'key2');
const token = tokens.sign(certs.bar.private, 'key2', {
iss: 'https://test.auth0.com/',
sub: '1234567890',
aud: 'https://test.auth0.com/api/v2/',
azp: 'https://test.auth0.com/api/v2/',
name: 'John Doe',
admin: true,
nonce: 'nonce'
});

const req = {
headers: {},
cookies: {
state_compat: 'state',
nonce_compat: 'nonce'
},
body: {
state: 'state',
id_token: token,
access_token: token
},
url: 'http://api/login/callback',
method: 'post'
};

const res = {
header: function() {},
clearCookie: function(name) {
if (name === 'nonce') t.equal(name, 'nonce');
else if (name === 'nonce_compat') t.equal(name, 'nonce_compat');
else if (name === 'state') t.equal(name, 'state');
else t.equal(name, 'state_compat');
},
status: function(status) {
return {
Expand Down Expand Up @@ -381,7 +536,9 @@ tape('dashboardAdmins should work with localStorage', function(t) {
header: function() {},
clearCookie: function(name) {
if (name === 'nonce') t.equal(name, 'nonce');
else t.equal(name, 'state');
else if (name === 'nonce_compat') t.equal(name, 'nonce_compat');
else if (name === 'state') t.equal(name, 'state');
else t.equal(name, 'state_compat');
},
status: function(status) {
return {
Expand Down
Loading

0 comments on commit 9f6c619

Please sign in to comment.