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;