diff --git a/src/routes/oauth2.js b/src/routes/oauth2.js
index 560f45328..be32da0e0 100644
--- a/src/routes/oauth2.js
+++ b/src/routes/oauth2.js
@@ -1,5 +1,24 @@
'use strict';
+exports = module.exports = {
+ initialize: initialize,
+ uninitialize: uninitialize,
+ loginForm: loginForm,
+ login: login,
+ logout: logout,
+ sessionCallback: sessionCallback,
+ passwordResetRequestSite: passwordResetRequestSite,
+ passwordResetRequest: passwordResetRequest,
+ passwordSentSite: passwordSentSite,
+ passwordResetSite: passwordResetSite,
+ passwordReset: passwordReset,
+ accountSetupSite: accountSetupSite,
+ accountSetup: accountSetup,
+ authorization: authorization,
+ token: token,
+ csrf: csrf
+};
+
var apps = require('../apps'),
assert = require('assert'),
authcodedb = require('../authcodedb'),
@@ -33,110 +52,120 @@ function auditSource(req, appId, appObject) {
}
// create OAuth 2.0 server
-var gServer = oauth2orize.createServer();
+var gServer = null;
-// Register serialialization and deserialization functions.
-//
-// The client id is stored in the session and can thus be retrieved for each
-// step in the oauth flow transaction, which involves multiple http requests.
+function initialize() {
+ assert(gServer === null);
-gServer.serializeClient(function (client, callback) {
- return callback(null, client.id);
-});
+ gServer = oauth2orize.createServer();
-gServer.deserializeClient(function (id, callback) {
- clients.get(id, callback);
-});
+ // Register serialialization and deserialization functions.
+ //
+ // The client id is stored in the session and can thus be retrieved for each
+ // step in the oauth flow transaction, which involves multiple http requests.
-
-// Register supported grant types.
-
-// Grant authorization codes. The callback takes the `client` requesting
-// authorization, the `redirectURI` (which is used as a verifier in the
-// subsequent exchange), the authenticated `user` granting access, and
-// their response, which contains approved scope, duration, etc. as parsed by
-// the application. The application issues a code, which is bound to these
-// values, and will be exchanged for an access token.
-
-gServer.grant(oauth2orize.grant.code({ scopeSeparator: ',' }, function (client, redirectURI, user, ares, callback) {
- debug('grant code:', client.id, redirectURI, user.id, ares);
-
- var code = hat(256);
- var expiresAt = Date.now() + 60 * 60000; // 1 hour
-
- authcodedb.add(code, client.id, user.id, expiresAt, function (error) {
- if (error) return callback(error);
-
- debug('grant code: new auth code for client %s code %s', client.id, code);
-
- callback(null, code);
+ gServer.serializeClient(function (client, callback) {
+ return callback(null, client.id);
});
-}));
-
-gServer.grant(oauth2orize.grant.token({ scopeSeparator: ',' }, function (client, user, ares, callback) {
- debug('grant token:', client.id, user.id, ares);
-
- var token = tokendb.generateToken();
- var expires = Date.now() + constants.DEFAULT_TOKEN_EXPIRATION;
-
- tokendb.add(token, user.id, client.id, expires, client.scope, function (error) {
- if (error) return callback(error);
-
- debug('grant token: new access token for client %s token %s', client.id, token);
-
- callback(null, token);
+ gServer.deserializeClient(function (id, callback) {
+ clients.get(id, callback);
});
-}));
-// Exchange authorization codes for access tokens. The callback accepts the
-// `client`, which is exchanging `code` and any `redirectURI` from the
-// authorization request for verification. If these values are validated, the
-// application issues an access token on behalf of the user who authorized the
-// code.
+ // Register supported grant types.
-gServer.exchange(oauth2orize.exchange.code(function (client, code, redirectURI, callback) {
- debug('exchange:', client, code, redirectURI);
+ // Grant authorization codes. The callback takes the `client` requesting
+ // authorization, the `redirectURI` (which is used as a verifier in the
+ // subsequent exchange), the authenticated `user` granting access, and
+ // their response, which contains approved scope, duration, etc. as parsed by
+ // the application. The application issues a code, which is bound to these
+ // values, and will be exchanged for an access token.
- authcodedb.get(code, function (error, authCode) {
- if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, false);
- if (error) return callback(error);
- if (client.id !== authCode.clientId) return callback(null, false);
+ gServer.grant(oauth2orize.grant.code({ scopeSeparator: ',' }, function (client, redirectURI, user, ares, callback) {
+ debug('grant code:', client.id, redirectURI, user.id, ares);
- authcodedb.del(code, function (error) {
- if(error) return callback(error);
+ var code = hat(256);
+ var expiresAt = Date.now() + 60 * 60000; // 1 hour
- var token = tokendb.generateToken();
- var expires = Date.now() + constants.DEFAULT_TOKEN_EXPIRATION;
+ authcodedb.add(code, client.id, user.id, expiresAt, function (error) {
+ if (error) return callback(error);
- tokendb.add(token, authCode.userId, authCode.clientId, expires, client.scope, function (error) {
- if (error) return callback(error);
+ debug('grant code: new auth code for client %s code %s', client.id, code);
- debug('exchange: new access token for client %s token %s', client.id, token);
+ callback(null, code);
+ });
+ }));
- callback(null, token);
+
+ gServer.grant(oauth2orize.grant.token({ scopeSeparator: ',' }, function (client, user, ares, callback) {
+ debug('grant token:', client.id, user.id, ares);
+
+ var token = tokendb.generateToken();
+ var expires = Date.now() + constants.DEFAULT_TOKEN_EXPIRATION;
+
+ tokendb.add(token, user.id, client.id, expires, client.scope, function (error) {
+ if (error) return callback(error);
+
+ debug('grant token: new access token for client %s token %s', client.id, token);
+
+ callback(null, token);
+ });
+ }));
+
+
+ // Exchange authorization codes for access tokens. The callback accepts the
+ // `client`, which is exchanging `code` and any `redirectURI` from the
+ // authorization request for verification. If these values are validated, the
+ // application issues an access token on behalf of the user who authorized the
+ // code.
+
+ gServer.exchange(oauth2orize.exchange.code(function (client, code, redirectURI, callback) {
+ debug('exchange:', client, code, redirectURI);
+
+ authcodedb.get(code, function (error, authCode) {
+ if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, false);
+ if (error) return callback(error);
+ if (client.id !== authCode.clientId) return callback(null, false);
+
+ authcodedb.del(code, function (error) {
+ if(error) return callback(error);
+
+ var token = tokendb.generateToken();
+ var expires = Date.now() + constants.DEFAULT_TOKEN_EXPIRATION;
+
+ tokendb.add(token, authCode.userId, authCode.clientId, expires, client.scope, function (error) {
+ if (error) return callback(error);
+
+ debug('exchange: new access token for client %s token %s', client.id, token);
+
+ callback(null, token);
+ });
});
});
- });
-}));
+ }));
-// overwrite the session.ensureLoggedIn to not use res.redirect() due to a chrome bug not sending cookies on redirects
-session.ensureLoggedIn = function (redirectTo) {
- assert.strictEqual(typeof redirectTo, 'string');
+ // overwrite the session.ensureLoggedIn to not use res.redirect() due to a chrome bug not sending cookies on redirects
+ session.ensureLoggedIn = function (redirectTo) {
+ assert.strictEqual(typeof redirectTo, 'string');
- return function (req, res, next) {
- if (!req.isAuthenticated || !req.isAuthenticated()) {
- if (req.session) {
- req.session.returnTo = req.originalUrl || req.url;
+ return function (req, res, next) {
+ if (!req.isAuthenticated || !req.isAuthenticated()) {
+ if (req.session) {
+ req.session.returnTo = req.originalUrl || req.url;
+ }
+
+ res.status(200).send(util.format('', redirectTo));
+ } else {
+ next();
}
-
- res.status(200).send(util.format('', redirectTo));
- } else {
- next();
- }
+ };
};
-};
+}
+
+function uninitialize() {
+ gServer = null;
+}
function renderTemplate(res, template, data) {
assert.strictEqual(typeof res, 'object');
@@ -415,13 +444,14 @@ function passwordReset(req, res, next) {
// The callback page takes the redirectURI and the authCode and redirects the browser accordingly
//
// -> GET /api/v1/session/callback
-var callback = [
- session.ensureLoggedIn('/api/v1/session/login'),
- function (req, res) {
- renderTemplate(res, 'callback', { callbackServer: req.query.redirectURI });
- }
-];
-
+function sessionCallback() {
+ return [
+ session.ensureLoggedIn('/api/v1/session/login'),
+ function (req, res) {
+ renderTemplate(res, 'callback', { callbackServer: req.query.redirectURI });
+ }
+ ];
+}
// The authorization endpoint is the entry point for an OAuth login.
//
@@ -433,54 +463,55 @@ var callback = [
// - Then it will redirect the browser to the given containing the authcode in the query
//
// -> GET /api/v1/oauth/dialog/authorize
-var authorization = [
- function (req, res, next) {
- if (!req.query.redirect_uri) return sendErrorPageOrRedirect(req, res, 'Invalid request. redirect_uri query param is not set.');
- if (!req.query.client_id) return sendErrorPageOrRedirect(req, res, 'Invalid request. client_id query param is not set.');
- if (!req.query.response_type) return sendErrorPageOrRedirect(req, res, 'Invalid request. response_type query param is not set.');
- if (req.query.response_type !== 'code' && req.query.response_type !== 'token') return sendErrorPageOrRedirect(req, res, 'Invalid request. Only token and code response types are supported.');
+function authorization() {
+ return [
+ function (req, res, next) {
+ if (!req.query.redirect_uri) return sendErrorPageOrRedirect(req, res, 'Invalid request. redirect_uri query param is not set.');
+ if (!req.query.client_id) return sendErrorPageOrRedirect(req, res, 'Invalid request. client_id query param is not set.');
+ if (!req.query.response_type) return sendErrorPageOrRedirect(req, res, 'Invalid request. response_type query param is not set.');
+ if (req.query.response_type !== 'code' && req.query.response_type !== 'token') return sendErrorPageOrRedirect(req, res, 'Invalid request. Only token and code response types are supported.');
- session.ensureLoggedIn('/api/v1/session/login?returnTo=' + req.query.redirect_uri)(req, res, next);
- },
- gServer.authorization({}, function (clientId, redirectURI, callback) {
- debug('authorization: client %s with callback to %s.', clientId, redirectURI);
+ session.ensureLoggedIn('/api/v1/session/login?returnTo=' + req.query.redirect_uri)(req, res, next);
+ },
+ gServer.authorization({}, function (clientId, redirectURI, callback) {
+ debug('authorization: client %s with callback to %s.', clientId, redirectURI);
- clients.get(clientId, function (error, client) {
- if (error && error.reason === ClientsError.NOT_FOUND) return callback(null, false);
- if (error) return callback(error);
+ clients.get(clientId, function (error, client) {
+ if (error && error.reason === ClientsError.NOT_FOUND) return callback(null, false);
+ if (error) return callback(error);
- // ignore the origin passed into form the client, but use the one from the clientdb
- var redirectPath = url.parse(redirectURI).path;
- var redirectOrigin = client.redirectURI;
+ // ignore the origin passed into form the client, but use the one from the clientdb
+ var redirectPath = url.parse(redirectURI).path;
+ var redirectOrigin = client.redirectURI;
- callback(null, client, '/api/v1/session/callback?redirectURI=' + encodeURIComponent(url.resolve(redirectOrigin, redirectPath)));
- });
- }),
- function (req, res, next) {
- // Handle our different types of oauth clients
- var type = req.oauth2.client.type;
-
- if (type === clients.TYPE_EXTERNAL || type === clients.TYPE_BUILT_IN) {
- eventlog.add(eventlog.ACTION_USER_LOGIN, auditSource(req, req.oauth2.client.appId), { userId: req.oauth2.user.id, user: users.removePrivateFields(req.oauth2.user) });
- return next();
- }
-
- apps.get(req.oauth2.client.appId, function (error, appObject) {
- if (error) return sendErrorPageOrRedirect(req, res, 'Invalid request. Unknown app for this client_id.');
-
- apps.hasAccessTo(appObject, req.oauth2.user, function (error, access) {
- if (error) return sendError(req, res, 'Internal error');
- if (!access) return sendErrorPageOrRedirect(req, res, 'No access to this app.');
-
- eventlog.add(eventlog.ACTION_USER_LOGIN, auditSource(req, appObject.id, appObject), { userId: req.oauth2.user.id, user: users.removePrivateFields(req.oauth2.user) });
-
- next();
+ callback(null, client, '/api/v1/session/callback?redirectURI=' + encodeURIComponent(url.resolve(redirectOrigin, redirectPath)));
});
- });
- },
- gServer.decision({ loadTransaction: false })
-];
+ }),
+ function (req, res, next) {
+ // Handle our different types of oauth clients
+ var type = req.oauth2.client.type;
+ if (type === clients.TYPE_EXTERNAL || type === clients.TYPE_BUILT_IN) {
+ eventlog.add(eventlog.ACTION_USER_LOGIN, auditSource(req, req.oauth2.client.appId), { userId: req.oauth2.user.id, user: users.removePrivateFields(req.oauth2.user) });
+ return next();
+ }
+
+ apps.get(req.oauth2.client.appId, function (error, appObject) {
+ if (error) return sendErrorPageOrRedirect(req, res, 'Invalid request. Unknown app for this client_id.');
+
+ apps.hasAccessTo(appObject, req.oauth2.user, function (error, access) {
+ if (error) return sendError(req, res, 'Internal error');
+ if (!access) return sendErrorPageOrRedirect(req, res, 'No access to this app.');
+
+ eventlog.add(eventlog.ACTION_USER_LOGIN, auditSource(req, appObject.id, appObject), { userId: req.oauth2.user.id, user: users.removePrivateFields(req.oauth2.user) });
+
+ next();
+ });
+ });
+ },
+ gServer.decision({ loadTransaction: false })
+ ];
+}
// The token endpoint allows an OAuth client to exchange an authcode with an accesstoken.
//
@@ -489,35 +520,22 @@ var authorization = [
// An authcode is only good for one such exchange to an accesstoken.
//
// -> POST /api/v1/oauth/token
-var token = [
- passport.authenticate(['basic', 'oauth2-client-password'], { session: false }),
- gServer.token(),
- gServer.errorHandler()
-];
+function token() {
+ return [
+ passport.authenticate(['basic', 'oauth2-client-password'], { session: false }),
+ gServer.token(),
+ gServer.errorHandler()
+ ];
+}
// Cross-site request forgery protection middleware for login form
-var csrf = [
- middleware.csrf(),
- function (err, req, res, next) {
- if (err.code !== 'EBADCSRFTOKEN') return next(err);
+function csrf() {
+ return [
+ middleware.csrf(),
+ function (err, req, res, next) {
+ if (err.code !== 'EBADCSRFTOKEN') return next(err);
- sendErrorPageOrRedirect(req, res, 'Form expired');
- }
-];
-
-exports = module.exports = {
- loginForm: loginForm,
- login: login,
- logout: logout,
- callback: callback,
- passwordResetRequestSite: passwordResetRequestSite,
- passwordResetRequest: passwordResetRequest,
- passwordSentSite: passwordSentSite,
- passwordResetSite: passwordResetSite,
- passwordReset: passwordReset,
- accountSetupSite: accountSetupSite,
- accountSetup: accountSetup,
- authorization: authorization,
- token: token,
- csrf: csrf
-};
+ sendErrorPageOrRedirect(req, res, 'Form expired');
+ }
+ ];
+}
diff --git a/src/routes/test/clients-test.js b/src/routes/test/clients-test.js
index 4a25a19af..e5b9128cd 100644
--- a/src/routes/test/clients-test.js
+++ b/src/routes/test/clients-test.js
@@ -360,9 +360,11 @@ describe('Clients', function () {
};
// make csrf always succeed for testing
- oauth2.csrf = function (req, res, next) {
- req.csrfToken = function () { return hat(256); };
- next();
+ oauth2.csrf = function () {
+ return function (req, res, next) {
+ req.csrfToken = function () { return hat(256); };
+ next();
+ };
};
function setup2(done) {
diff --git a/src/routes/test/oauth2-test.js b/src/routes/test/oauth2-test.js
index 4d64b478b..f704f57c8 100644
--- a/src/routes/test/oauth2-test.js
+++ b/src/routes/test/oauth2-test.js
@@ -189,9 +189,11 @@ describe('OAuth2', function () {
};
// make csrf always succeed for testing
- oauth2.csrf = function (req, res, next) {
- req.csrfToken = function () { return hat(256); };
- next();
+ oauth2.csrf = function () {
+ return function (req, res, next) {
+ req.csrfToken = function () { return hat(256); };
+ next();
+ };
};
function setup(done) {
@@ -1281,9 +1283,11 @@ describe('Password', function () {
};
// make csrf always succeed for testing
- oauth2.csrf = function (req, res, next) {
- req.csrfToken = function () { return hat(256); };
- next();
+ oauth2.csrf = function () {
+ return function (req, res, next) {
+ req.csrfToken = function () { return hat(256); };
+ next();
+ };
};
function setup(done) {
diff --git a/src/server.js b/src/server.js
index 7279159db..e9c94d517 100644
--- a/src/server.js
+++ b/src/server.js
@@ -100,7 +100,7 @@ function initializeExpressSync() {
var domainsScope = routes.accesscontrol.scope(accesscontrol.SCOPE_DOMAINS);
// csrf protection
- var csrf = routes.oauth2.csrf;
+ var csrf = routes.oauth2.csrf();
// public routes
router.post('/api/v1/cloudron/dns_setup', routes.setup.providerTokenAuth, routes.setup.dnsSetup); // only available until no-domain
@@ -158,7 +158,7 @@ function initializeExpressSync() {
router.get ('/api/v1/session/login', csrf, routes.oauth2.loginForm);
router.post('/api/v1/session/login', csrf, routes.oauth2.login);
router.get ('/api/v1/session/logout', routes.oauth2.logout);
- router.get ('/api/v1/session/callback', routes.oauth2.callback);
+ router.get ('/api/v1/session/callback', routes.oauth2.sessionCallback());
router.get ('/api/v1/session/password/resetRequest.html', csrf, routes.oauth2.passwordResetRequestSite);
router.post('/api/v1/session/password/resetRequest', csrf, routes.oauth2.passwordResetRequest);
router.get ('/api/v1/session/password/sent.html', routes.oauth2.passwordSentSite);
@@ -168,8 +168,8 @@ function initializeExpressSync() {
router.post('/api/v1/session/account/setup', csrf, routes.oauth2.accountSetup);
// oauth2 routes
- router.get ('/api/v1/oauth/dialog/authorize', routes.oauth2.authorization);
- router.post('/api/v1/oauth/token', routes.oauth2.token);
+ router.get ('/api/v1/oauth/dialog/authorize', routes.oauth2.authorization());
+ router.post('/api/v1/oauth/token', routes.oauth2.token());
router.get ('/api/v1/oauth/clients', clientsScope, routes.clients.getAll);
router.post('/api/v1/oauth/clients', clientsScope, routes.clients.add);
@@ -330,6 +330,8 @@ function start(callback) {
assert.strictEqual(typeof callback, 'function');
assert.strictEqual(gHttpServer, null, 'Server is already up and running.');
+ routes.oauth2.initialize();
+
gHttpServer = initializeExpressSync();
gSysadminHttpServer = initializeSysadminExpressSync();
@@ -358,6 +360,8 @@ function stop(callback) {
], function (error) {
if (error) console.error(error);
+ routes.oauth2.uninitialize();
+
gHttpServer = null;
gSysadminHttpServer = null;