diff --git a/app/app/scripts/app.js b/app/app/scripts/app.js new file mode 100644 index 00000000..9e34d041 --- /dev/null +++ b/app/app/scripts/app.js @@ -0,0 +1,11 @@ +define([ + 'angular' + , './controllers/index' + ] +, function(ng) { + return ng.module('app', [ + 'ngRoute' + , 'app.controllers' + ]) + } +) diff --git a/app/app/scripts/bootstrap.js b/app/app/scripts/bootstrap.js new file mode 100644 index 00000000..19442318 --- /dev/null +++ b/app/app/scripts/bootstrap.js @@ -0,0 +1,11 @@ +define([ + 'require' + , 'angular' + , 'angular-route' + , 'app' + , 'routes' + ] +, function(require, ng) { + ng.bootstrap(document, ['app']) + } +) diff --git a/app/app/scripts/controllers/index.js b/app/app/scripts/controllers/index.js new file mode 100644 index 00000000..44e3831b --- /dev/null +++ b/app/app/scripts/controllers/index.js @@ -0,0 +1,5 @@ +define([ + ] +, function() { + } +) diff --git a/app/app/scripts/controllers/module.js b/app/app/scripts/controllers/module.js new file mode 100644 index 00000000..dc663078 --- /dev/null +++ b/app/app/scripts/controllers/module.js @@ -0,0 +1,3 @@ +define(['angular'], function(ng) { + return ng.module('app.controllers', []) +}) diff --git a/app/app/scripts/main.js b/app/app/scripts/main.js new file mode 100644 index 00000000..8fd793b2 --- /dev/null +++ b/app/app/scripts/main.js @@ -0,0 +1,19 @@ +require.config({ + paths: { + 'angular': '../lib/angular/angular' + , 'angular-route': '../lib/angular-route/angular-route' + } +, shim: { + 'angular': { + exports: 'angular' + } + , 'angular-route': { + deps: [ + 'angular' + ] + } + } +, deps: [ + './bootstrap' + ] +}) diff --git a/app/app/scripts/routes.js b/app/app/scripts/routes.js new file mode 100644 index 00000000..d207b550 --- /dev/null +++ b/app/app/scripts/routes.js @@ -0,0 +1,17 @@ +define(['./app'], function(app) { + return app.config([ + '$routeProvider' + , '$locationProvider' + , function($routeProvider, $locationProvider) { + $locationProvider.html5Mode(true) + $routeProvider + .when('/', { + templateUrl: 'partials/signin' + , controller: 'SignInCtrl' + }) + .otherwise({ + redirectTo: '/' + }) + } + ]) +}) diff --git a/app/app/views/index.jade b/app/app/views/index.jade new file mode 100644 index 00000000..dbb057e4 --- /dev/null +++ b/app/app/views/index.jade @@ -0,0 +1,7 @@ +doctype html +html + head + meta(charset='utf-8') + body(ng-cloak) + div(ng-view) + script(src='/static/lib/requirejs/require.js', data-main='static/scripts/main.js') diff --git a/app/auth/scripts/controllers/SignInCtrl.js b/app/auth/scripts/controllers/SignInCtrl.js index 47496bcf..3a525b96 100644 --- a/app/auth/scripts/controllers/SignInCtrl.js +++ b/app/auth/scripts/controllers/SignInCtrl.js @@ -6,7 +6,6 @@ define(['./module'], function(mod) { var data = { name: $scope.signin.name.$modelValue , email: $scope.signin.email.$modelValue - , redirect: $scope.signin.redirect.$modelValue } $scope.invalid = false $http.post('/api/v1/auth', data) diff --git a/app/auth/views/partials/signin.jade b/app/auth/views/partials/signin.jade index 10693118..a7f699b3 100644 --- a/app/auth/views/partials/signin.jade +++ b/app/auth/views/partials/signin.jade @@ -15,5 +15,4 @@ form(name='signin', novalidate, ng-submit='submit()') span(ng-show='signin.email.$error.email') Please enter a valid email address span(ng-show='signin.email.$error.required') Please enter your email address div - input(type='text', name='redirect', ng-model='redirect') button(type='submit') Sign In diff --git a/lib/cli.js b/lib/cli.js index 53ad9495..d41a83e4 100644 --- a/lib/cli.js +++ b/lib/cli.js @@ -1,3 +1,5 @@ +var util = require('util') + var program = require('commander') var pkg = require('../package') @@ -131,7 +133,7 @@ program .option('-p, --port ' , 'port (or $PORT)' , Number - , 7100) + , 7120) .option('-s, --secret ' , 'secret (or $SECRET)' , String) @@ -189,6 +191,41 @@ program program .command('auth-mock') .description('start mock auth client') + .option('-p, --port ' + , 'port (or $PORT)' + , Number + , 7120) + .option('-s, --secret ' + , 'secret (or $SECRET)' + , String) + .option('-i, --ssid ' + , 'session SSID (or $SSID)' + , String + , 'ssid') + .option('-a, --app-url ' + , 'URL to app' + , String) + .action(function(options) { + var env = process.env + + if (!options.secret) { + this.missingArgument('--secret') + } + if (!options.appUrl) { + this.missingArgument('--app-url') + } + + require('./roles/auth/mock')({ + port: env.PORT || options.port + , secret: options.secret || env.SECRET + , ssid: options.ssid || env.SSID + , appUrl: options.appUrl + }) + }) + +program + .command('app') + .description('start app') .option('-p, --port ' , 'port (or $PORT)' , Number @@ -200,12 +237,24 @@ program , 'session SSID (or $SSID)' , String , 'ssid') + .option('-a, --auth-url ' + , 'URL to auth client' + , String) .action(function(options) { var env = process.env - require('./roles/auth/mock')({ + + if (!options.secret) { + this.missingArgument('--secret') + } + if (!options.authUrl) { + this.missingArgument('--auth-url') + } + + require('./roles/app')({ port: env.PORT || options.port , secret: options.secret || env.SECRET , ssid: options.ssid || env.SSID + , authUrl: options.authUrl }) }) @@ -269,6 +318,10 @@ program , 'auth secret' , String , 'kute kittykat') + .option('--app-port ' + , 'app port' + , Number + , 7100) .action(function() { var log = logger.createLogger('cli') , options = cliutil.lastArg(arguments) @@ -330,10 +383,22 @@ program 'auth-mock' , '--port', options.authPort , '--secret', options.authSecret + , '--app-url', util.format('http://localhost:%d/', options.appPort) ]) .catch(function(err) { log.error('auth-mock died', err.stack) }) + + // app + procutil.fork(__filename, [ + 'app' + , '--port', options.appPort + , '--secret', options.authSecret + , '--auth-url', util.format('http://localhost:%d/', options.authPort) + ]) + .catch(function(err) { + log.error('app died', err.stack) + }) }) program.parse(process.argv) diff --git a/lib/middleware/jwt.js b/lib/middleware/jwt.js new file mode 100644 index 00000000..353fc137 --- /dev/null +++ b/lib/middleware/jwt.js @@ -0,0 +1,29 @@ +var jwtutil = require('../util/jwtutil') +var urlutil = require('../util/urlutil') + +module.exports = function(options) { + return function(req, res, next) { + if (req.query.jwt) { + // Coming from auth client + var data = jwtutil.decode(req.query.jwt, options.secret) + , redir = urlutil.removeParam(req.url, 'jwt') + if (data) { + // Redirect once to get rid of the token + req.session.jwt = data + res.redirect(redir) + } + else { + // Invalid token, forward to auth client + res.redirect(options.authUrl) + } + } + else if (req.session && req.session.jwt) { + // Continue existing session + next() + } + else { + // No session, forward to auth client + res.redirect(options.authUrl) + } + } +} diff --git a/lib/roles/app.js b/lib/roles/app.js new file mode 100644 index 00000000..0f5799c5 --- /dev/null +++ b/lib/roles/app.js @@ -0,0 +1,43 @@ +var url = require('url') + +var express = require('express') +var validator = require('express-validator') + +var logger = require('../util/logger') +var pathutil = require('../util/pathutil') + +var jwt = require('../middleware/jwt') + +module.exports = function(options) { + var log = logger.createLogger('app') + , app = express() + + app.set('view engine', 'jade') + app.set('views', pathutil.resource('app/views')) + app.set('strict routing', true) + app.set('case sensitive routing', true) + + app.use(express.cookieParser()) + app.use(express.cookieSession({ + secret: options.secret + , key: options.ssid + })) + app.use(jwt({ + secret: options.secret + , appUrl: options.url + , authUrl: options.authUrl + })) + app.use(express.json()) + app.use(express.urlencoded()) + app.use(express.csrf()) + app.use(validator()) + app.use('/static/lib', express.static(pathutil.resource('lib'))) + app.use('/static', express.static(pathutil.resource('app'))) + + app.get('/', function(req, res) { + res.render('index') + }) + + app.listen(options.port) + log.info('Listening on port %d', options.port) +} diff --git a/lib/roles/auth/ldap.js b/lib/roles/auth/ldap.js index bf640165..87651dbd 100644 --- a/lib/roles/auth/ldap.js +++ b/lib/roles/auth/ldap.js @@ -7,6 +7,7 @@ var logger = require('../../util/logger') var requtil = require('../../util/requtil') var ldaputil = require('../../util/ldaputil') var jwtutil = require('../../util/jwtutil') +var urlutil = require('../../util/urlutil') module.exports = function(options) { var log = logger.createLogger('auth-ldap') @@ -33,12 +34,6 @@ module.exports = function(options) { requtil.validate(req, function() { req.checkBody('username').notEmpty() req.checkBody('password').notEmpty() - - // This is a security risk. Someone might forward the user - // to the login page with their own redirect set, and they'd - // then be able to steal the token. Some kind of a whitelist - // or a fixed redirect URL is needed. - req.checkBody('redirect').isUrl() }) .then(function() { return ldaputil.login( @@ -56,14 +51,12 @@ module.exports = function(options) { } , secret: options.secret }) - var target = url.parse(req.body.redirect) - target.query = { - jwt: token - } res.status(200) .json({ success: true - , redirect: url.format(target) + , redirect: urlutil.addParams(options.appUrl, { + jwt: token + }) }) }) .catch(requtil.ValidationError, function(err) { diff --git a/lib/roles/auth/mock.js b/lib/roles/auth/mock.js index 9c6350d6..ea1b40a8 100644 --- a/lib/roles/auth/mock.js +++ b/lib/roles/auth/mock.js @@ -7,6 +7,7 @@ var logger = require('../../util/logger') var requtil = require('../../util/requtil') var jwtutil = require('../../util/jwtutil') var pathutil = require('../../util/pathutil') +var urlutil = require('../../util/urlutil') module.exports = function(options) { var log = logger.createLogger('auth-mock') @@ -59,12 +60,6 @@ module.exports = function(options) { requtil.validate(req, function() { req.checkBody('name').notEmpty() req.checkBody('email').isEmail() - - // This is a security risk. Someone might forward the user - // to the login page with their own redirect set, and they'd - // then be able to steal the token. Some kind of a whitelist - // or a fixed redirect URL is needed. - req.checkBody('redirect').isUrl() }) .then(function() { log.info('Authenticated "%s"', req.body.email) @@ -75,14 +70,12 @@ module.exports = function(options) { } , secret: options.secret }) - var target = url.parse(req.body.redirect) - target.query = { - jwt: token - } res.status(200) .json({ success: true - , redirect: url.format(target) + , redirect: urlutil.addParams(options.appUrl, { + jwt: token + }) }) }) .catch(requtil.ValidationError, function(err) { diff --git a/lib/util/urlutil.js b/lib/util/urlutil.js new file mode 100644 index 00000000..64073bcb --- /dev/null +++ b/lib/util/urlutil.js @@ -0,0 +1,17 @@ +var url = require('url') + +module.exports.addParams = function(originalUrl, params) { + var parsed = url.parse(originalUrl, true) + parsed.search = null + for (var key in params) { + parsed.query[key] = params[key] + } + return url.format(parsed) +} + +module.exports.removeParam = function(originalUrl, param) { + var parsed = url.parse(originalUrl, true) + parsed.search = null + delete parsed.query[param] + return url.format(parsed) +}