Skip to content

Commit

Permalink
feat(Initializer): Cleaning up config obj needed
Browse files Browse the repository at this point in the history
Cleaning up config obj needed while still allowing for existing clients to function after updating

BREAKING CHANGE: Anyone using the auth0verification file directly and not through the index.js

#23
  • Loading branch information
tocco934 committed May 1, 2017
1 parent 05588a2 commit 927ac7b
Show file tree
Hide file tree
Showing 8 changed files with 239 additions and 78 deletions.
29 changes: 11 additions & 18 deletions lib/auth0verification.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,38 +60,31 @@ const getAuthPublicKey = function (jwksUrl, cache, kid) {
// secures the application with auth0, by implementing checks against
// incoming jwt's, with configuration of what routes to apply it to
module.exports = function (app, config, logger, cache) {

// make sure we are configured to use auth0, otherwise, we dont need to
// actually apply anything
const auth0AppConfig = _.get(config, 'app.auth0.application');
const auth0Domain = _.get(config, 'app.auth0.domain');
const jwksUrl = _.get(config, 'app.auth0.jwksUrl');

if (auth0AppConfig) {
if (config && config.secret) {
// decode the client secret if it is base64 encoded
var secret;
if (auth0AppConfig.secret.match(BASE64_REGEX)) {
secret = new Buffer(auth0AppConfig.secret, 'base64');
if (config.secret.match(BASE64_REGEX)) {
secret = new Buffer(config.secret, 'base64');
} else {
secret = auth0AppConfig.secret;
secret = config.secret;
}

// set up jwt validation, which will take into account excluded routes
var v1JwtCheck = expressJwt({
secret: secret,
audience: auth0AppConfig.clientId
audience: config.clientId
});

let v2JwtCheck;
if (auth0AppConfig.resourceServer && jwksUrl) {
if (config.audience && config.jwksUrl) {
v2JwtCheck = expressJwt({
// Look up the public key to use based on the KID (key id) contained in the
// header of the JWT.
secret: (req, header, _payload, done) => {
if (!header || !header.kid) {
return done(new Error("JWT KID Not Found"));
}
getAuthPublicKey(jwksUrl, cache, header.kid).then(
getAuthPublicKey(config.jwksUrl, cache, header.kid).then(
(key) => {
if (key) {
return done(null, key);
Expand All @@ -101,8 +94,8 @@ module.exports = function (app, config, logger, cache) {
}, err => done(err)
);
},
audience: auth0AppConfig.resourceServer,
issuer: 'https://' + auth0Domain + '/',
audience: config.audience,
issuer: 'https://' + config.domain + '/',
algorithms: ['RS256']
});
}
Expand All @@ -113,7 +106,7 @@ module.exports = function (app, config, logger, cache) {
// 2) the request contains an `Authorization` header of the form "Bearer {JWT}"
// 3) the JWT is well formed and the algorithm used to sign it is "RS256" as specified
// by the `alg` field of the JWT header
if (auth0AppConfig.resourceServer && req.headers && req.headers.authorization) {
if (config.audience && req.headers && req.headers.authorization) {
let decodedToken;
try {
decodedToken = jwt.decode(req.headers.authorization.split(' ')[1], { complete: true });
Expand All @@ -131,7 +124,7 @@ module.exports = function (app, config, logger, cache) {
};
jwtAuthentication.unless = unless;

app.use(jwtAuthentication.unless({ path: getExcludedRoutes(auth0AppConfig.excludedRoutes) }));
app.use(jwtAuthentication.unless({ path: getExcludedRoutes(config.excludedRoutes) }));

require('./unauthorized')(app, config, logger);

Expand Down
21 changes: 17 additions & 4 deletions lib/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,20 @@
const auth0verification = require('./auth0verification');
const unauthorized = require('./unauthorized');
const _ = require('lodash');

module.exports = {
auth0verification,
unauthorized,
}
module.exports = (app, config, logger, cache) => {
return auth0verification(app, config, logger, cache);
};
module.exports.unauthorized = unauthorized;
module.exports.auth0verification = (app, legacyConfig, logger, cache) => {
const config = {
domain: _.get(legacyConfig, 'app.auth0.domain', undefined),
jwksUrl: _.get(legacyConfig, 'app.auth0.jwksUrl', undefined),
secret: _.get(legacyConfig, 'app.auth0.application.secret', undefined),
clientId: _.get(legacyConfig, 'app.auth0.application.clientId', undefined),
audience: _.get(legacyConfig, 'app.auth0.application.resourceServer', undefined),
excludedRoutes: _.get(legacyConfig, 'app.auth0.application.excludedRoutes', undefined),
realm: _.get(legacyConfig, 'app.auth0.realm', undefined),
};
return auth0verification(app, config, logger, cache);
};
72 changes: 34 additions & 38 deletions lib/unauthorized.js
Original file line number Diff line number Diff line change
@@ -1,38 +1,34 @@
var _ = require('lodash'),
util = require('util');

module.exports = function(app, config, logger) {
var auth0AppConfig = _.get(config, 'app.auth0.application');
var auth0Domain = _.get(config, 'app.auth0.domain');
var auth0Realm = _.get(config, 'app.auth0.realm');

app.use(function(err, req, res, next) {
if (err.name === 'UnauthorizedError') {
addWwwAuthenticateHeaders(req, res, auth0Domain, auth0AppConfig.clientId, auth0AppConfig.resourceServer, auth0Realm);
addLinkHeaders(res, auth0Domain, auth0AppConfig.resourceServer);
return res.status(401).json();
}
next(err);
});
};

function addWwwAuthenticateHeaders(req, res, domain, clientId, resourceServer, realm) {
var v1WwwAuthenticateHeader = util.format('Bearer realm="%s", scope="client_id=%s service=%s"',
domain, clientId, req.protocol + "://" + req.hostname + req.baseUrl);

if (resourceServer) {
var v2WwwAuthenticateHeader = util.format('Bearer realm="%s", authorization_uri="https://%s/oauth/token"', realm, domain);
return res.header('WWW-Authenticate', [v1WwwAuthenticateHeader, v2WwwAuthenticateHeader]);
}
res.header('WWW-Authenticate', v1WwwAuthenticateHeader);
}

function addLinkHeaders(res, domain, resourceServer) {
var v1authLink = '<https://%s/authorize>; rel="openid2.provider openid.server"';

if (resourceServer) {
var v2authLink = '<https://%s/oauth/token>; rel=authorization_uri';
return res.header('Link', [util.format(v1authLink, domain), util.format(v2authLink, domain)]);
}
res.header('Link', util.format(v1authLink, domain));
}
var _ = require('lodash'),
util = require('util');

module.exports = function(app, config, logger) {
app.use(function(err, req, res, next) {
if (err.name === 'UnauthorizedError') {
addWwwAuthenticateHeaders(req, res, config.domain, config.clientId, config.audience, config.realm);
addLinkHeaders(res, config.domain, config.audience);
return res.status(401).json();
}
next(err);
});
};

function addWwwAuthenticateHeaders(req, res, domain, clientId, resourceServer, realm) {
var v1WwwAuthenticateHeader = util.format('Bearer realm="%s", scope="client_id=%s service=%s"',
domain, clientId, req.protocol + "://" + req.hostname + req.baseUrl);

if (resourceServer) {
var v2WwwAuthenticateHeader = util.format('Bearer realm="%s", authorization_uri="https://%s/oauth/token"', realm, domain);
return res.header('WWW-Authenticate', [v1WwwAuthenticateHeader, v2WwwAuthenticateHeader]);
}
res.header('WWW-Authenticate', v1WwwAuthenticateHeader);
}

function addLinkHeaders(res, domain, resourceServer) {
var v1authLink = '<https://%s/authorize>; rel="openid2.provider openid.server"';

if (resourceServer) {
var v2authLink = '<https://%s/oauth/token>; rel=authorization_uri';
return res.header('Link', [util.format(v1authLink, domain), util.format(v2authLink, domain)]);
}
res.header('Link', util.format(v1authLink, domain));
}
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
},
"author": "Dave Purrington",
"license": "SEE LICENSE in LICENSE.txt",
"contributors": [{
"name": "Dan Tocco"
}],
"dependencies": {
"bluebird": "^3.5.0",
"express-jwt": "^5.1.0",
Expand Down
157 changes: 157 additions & 0 deletions test/auth0verification.legacy.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
/*jshint -W030 */
'use strict';
const expect = require("chai").expect,
chai = require("chai"),
assert = require("assert-plus"),
spies = require("chai-spies"),
JwtMock = require('./jwtmock.js'),
UnauthorizedError = require('express-jwt/lib/errors/UnauthorizedError'),
Helper = require("./supertesthelper.js");

chai.use(spies);

describe('Verify auth0 application functions with legacy config.', function () {

let helper, jwtMock, config, mw, defaultCache;
const clientId = "myId";
const domain = "cimpressfake.auth0.com";
const realm = "https://fakeapi.cimpress.io/";
const jwksUrl = "https://fakejwksserver.cimpress.io";
beforeEach(function () {
config = {
app: {
auth0: {
application: {
clientId: clientId,
connections: ['conn1', 'conn2'],
secret: 'this is a secret'
},
domain: domain,
realm: realm,
jwksUrl: jwksUrl,
}
}
};

defaultCache = {
get: () => Promise.resolve(undefined),
set: () => Promise.resolve(),
};

jwtMock = new JwtMock();

mw = require('../lib/index').auth0verification;
});

afterEach(function () {
jwtMock.tearDown();
helper = undefined;
});

it('Should validate a request with legacy config', function () {
jwtMock.setJwtFunction(function (req, res, next) {
req.user = {};
next();
});

helper = new Helper(mw, config, null, defaultCache);
helper.app.get("/stub", function (req, res) {
res.status(200).json({ name: "tobi" });
});
return helper.execute("/stub").expect(200).then(function (req, res) {
return expect(helper.finishedRequest.user).to.not.be.undefined;
});
});

it('Should reject an unauthenticated request with legacy config', function () {

jwtMock.setJwtFunction(function (req, res, next) {
return next(new UnauthorizedError('credentials_required', {
message: 'No authorization token was found'
}));
});
helper = new Helper(mw, config, null, defaultCache);
helper.app.get("/stub", function (req, res) {
res.status(200).json({ name: "tobi" });
});

return helper.execute("/stub").expect(401).then(function (req, res) {
expect(helper.finishedRequest.user).to.be.undefined;
expect(helper.finishedResponse._headers['www-authenticate']).to.include(
'Bearer realm="' + domain + '", scope="client_id=' + clientId +
' service=' + helper.finishedRequest.protocol +
'://' + helper.finishedRequest.hostname + helper.finishedRequest.baseUrl + '"');
});
});

it('Should return an appropriate WWW-Authenticate header for API Authentication with legacy config', function () {

jwtMock.setJwtFunction(function (req, res, next) {
return next(new UnauthorizedError('credentials_required', {
message: 'No authorization token was found'
}));
});
// Clone the config so as not to break other tests.
var v2config = JSON.parse(JSON.stringify(config));
v2config.app.auth0.application.resourceServer = 'http://api.cimpress.io/';

helper = new Helper(mw, v2config, null, defaultCache);
helper.app.get("/stub", function (req, res) {
res.status(200).json({ name: "tobi" });
});

return helper.execute("/stub").expect(401).then(function (req, res) {
expect(helper.finishedRequest.user).to.be.undefined;
expect(helper.finishedResponse._headers['www-authenticate']).to.include(
'Bearer realm="' + domain + '", scope="client_id=' + clientId +
' service=' + helper.finishedRequest.protocol +
'://' + helper.finishedRequest.hostname + helper.finishedRequest.baseUrl + '"');
expect(helper.finishedResponse._headers['www-authenticate']).to.include(
'Bearer realm="' + realm + '", authorization_uri="https://' + domain + '/oauth/token"');
});
});

it('Should be able to exclude a route with legacy config', function () {
jwtMock.setJwtFunction(function (req, res, next) {
req.user = {};
next();
});

var excludedRoute = "/excluded";
config.app.auth0.application.excludedRoutes = [excludedRoute];
helper = new Helper(mw, config, null, defaultCache);
helper.app.get(excludedRoute, function (req, res) {
res.status(200).json({});
});

return helper
.execute(excludedRoute).expect(200)
.then((req, res) => expect(helper.finishedRequest.user).to.be.undefined);
});

it('Should be able to exclude a route by HTTP method with legacy config', function () {
jwtMock.setJwtFunction(function (req, res, next) {
req.user = {};
next();
});

var excludedRoute = "/excluded";
config.app.auth0.application.excludedRoutes = [{
url: excludedRoute,
methods: ["GET"]
}];
helper = new Helper(mw, config, null, defaultCache);
helper.app.get(excludedRoute, function (req, res) {
res.status(200).json({});
});
helper.app.post(excludedRoute, function (req, res) {
res.status(200).json({});
});

return helper
.execute(excludedRoute).expect(200)
.then((req, res) => expect(helper.finishedRequest.user).to.be.undefined)
.then(() => helper.post(excludedRoute).expect(200))
.then((req, res) => expect(helper.finishedRequest.user).to.not.be.undefined);
});
});
Loading

0 comments on commit 927ac7b

Please sign in to comment.