From 9119dcca6348696ea311078f2b2b672a6aa8ecc0 Mon Sep 17 00:00:00 2001 From: Simo Kinnunen Date: Fri, 31 Jan 2014 21:39:07 +0900 Subject: [PATCH] Add UI to LDAP login. --- lib/cli.js | 23 ++++- lib/roles/auth/ldap.js | 33 +++++++- lib/util/ldaputil.js | 23 +++-- res/auth-ldap/images/logo-128.png | Bin 0 -> 8450 bytes res/auth-ldap/scripts/app.js | 11 +++ res/auth-ldap/scripts/bootstrap.js | 11 +++ .../scripts/controllers/SignInCtrl.js | 37 ++++++++ res/auth-ldap/scripts/controllers/index.js | 6 ++ res/auth-ldap/scripts/controllers/module.js | 3 + res/auth-ldap/scripts/main.js | 19 +++++ res/auth-ldap/scripts/routes.js | 17 ++++ res/auth-ldap/styles/login.css | 79 ++++++++++++++++++ res/auth-ldap/views/index.jade | 9 ++ res/auth-ldap/views/partials/signin.jade | 28 +++++++ res/auth-ldap/views/partials/styles.jade | 6 ++ 15 files changed, 291 insertions(+), 14 deletions(-) create mode 100644 res/auth-ldap/images/logo-128.png create mode 100644 res/auth-ldap/scripts/app.js create mode 100644 res/auth-ldap/scripts/bootstrap.js create mode 100644 res/auth-ldap/scripts/controllers/SignInCtrl.js create mode 100644 res/auth-ldap/scripts/controllers/index.js create mode 100644 res/auth-ldap/scripts/controllers/module.js create mode 100644 res/auth-ldap/scripts/main.js create mode 100644 res/auth-ldap/scripts/routes.js create mode 100644 res/auth-ldap/styles/login.css create mode 100644 res/auth-ldap/views/index.jade create mode 100644 res/auth-ldap/views/partials/signin.jade create mode 100644 res/auth-ldap/views/partials/styles.jade diff --git a/lib/cli.js b/lib/cli.js index 18e7ada6..5fc479bf 100644 --- a/lib/cli.js +++ b/lib/cli.js @@ -194,6 +194,9 @@ program , 'session SSID (or $SSID)' , String , process.env.SSID || 'ssid') + .option('-a, --app-url ' + , 'URL to app' + , String) .option('-u, --ldap-url ' , 'LDAP server URL (or $LDAP_URL)' , String @@ -202,6 +205,10 @@ program , 'LDAP timeout (or $LDAP_TIMEOUT)' , Number , process.env.LDAP_TIMEOUT || 1000) + .option('--ldap-bind-enable ' + , 'LDAP bind DN (or $LDAP_BIND_DN)' + , String + , process.env.LDAP_BIND_DN) .option('--ldap-bind-dn ' , 'LDAP bind DN (or $LDAP_BIND_DN)' , String @@ -221,12 +228,24 @@ program .option('--ldap-search-class ' , 'LDAP search objectClass (or $LDAP_SEARCH_CLASS)' , String - , process.env.LDAP_SEARCH_CLASS || 'user') + , process.env.LDAP_SEARCH_CLASS || 'top') + .option('--ldap-search-field ' + , 'LDAP search field (or $LDAP_SEARCH_FIELD)' + , String + , process.env.LDAP_SEARCH_FIELD) .action(function(options) { + if (!options.secret) { + this.missingArgument('--secret') + } + if (!options.appUrl) { + this.missingArgument('--app-url') + } + require('./roles/auth/ldap')({ port: options.port , secret: options.secret , ssid: options.ssid + , appUrl: options.appUrl , ldap: { url: options.ldapUrl , timeout: options.ldapTimeout @@ -238,7 +257,7 @@ program dn: options.ldapSearchDn , scope: options.ldapSearchScope , objectClass: options.ldapSearchClass - , loginField: options.ldapSearchLoginField + , field: options.ldapSearchField } } }) diff --git a/lib/roles/auth/ldap.js b/lib/roles/auth/ldap.js index 87651dbd..faabb283 100644 --- a/lib/roles/auth/ldap.js +++ b/lib/roles/auth/ldap.js @@ -7,12 +7,18 @@ var logger = require('../../util/logger') var requtil = require('../../util/requtil') var ldaputil = require('../../util/ldaputil') var jwtutil = require('../../util/jwtutil') +var pathutil = require('../../util/pathutil') var urlutil = require('../../util/urlutil') module.exports = function(options) { var log = logger.createLogger('auth-ldap') , app = express() + app.set('view engine', 'jade') + app.set('views', pathutil.resource('auth-ldap/views')) + app.set('strict routing', true) + app.set('case sensitive routing', true) + app.use(express.cookieParser()) app.use(express.cookieSession({ secret: options.secret @@ -20,13 +26,34 @@ module.exports = function(options) { })) 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('auth-ldap'))) - app.get('/auth', function(req, res) { - res.locals.csrf = req.csrfToken() + app.use(function(req, res, next) { + res.cookie('XSRF-TOKEN', req.csrfToken()); + next() }) - app.post('/auth', function(req, res) { + app.get('/partials/:name', function(req, res) { + var whitelist = { + 'signin': true + } + + if (whitelist[req.params.name]) { + res.render('partials/' + req.params.name) + } + else { + res.send(404) + } + }) + + app.get('/', function(req, res) { + res.render('index') + }) + + app.post('/api/v1/auth', function(req, res) { var log = logger.createLogger('auth-ldap') log.setLocalIdentifier(req.ip) switch (req.accepts(['json'])) { diff --git a/lib/util/ldaputil.js b/lib/util/ldaputil.js index e188fdc7..a13bb97d 100644 --- a/lib/util/ldaputil.js +++ b/lib/util/ldaputil.js @@ -25,14 +25,19 @@ module.exports.login = function(options, username, password) { , maxConnections: 1 }) - client.bind(options.bind.dn, options.bind.credentials, function(err) { - if (err) { - resolver.reject(err) - } - else { - resolver.resolve(client) - } - }) + if (options.bind.dn) { + client.bind(options.bind.dn, options.bind.credentials, function(err) { + if (err) { + resolver.reject(err) + } + else { + resolver.resolve(client) + } + }) + } + else { + resolver.resolve(client) + } return resolver.promise } @@ -48,7 +53,7 @@ module.exports.login = function(options, username, password) { , value: options.search.objectClass }) , new ldap.EqualityFilter({ - attribute: options.search.loginField + attribute: options.search.field , value: username }) ] diff --git a/res/auth-ldap/images/logo-128.png b/res/auth-ldap/images/logo-128.png new file mode 100644 index 0000000000000000000000000000000000000000..e360432e4f29b16b67e8f8e172419eb35e973b2a GIT binary patch literal 8450 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 .btn { + width: 49%; + margin: 0; +} + +.login2 .login-wrapper .social-login .facebook { + background-color: #335397; + border-color: #335397; +} + +.login2 .login-wrapper .social-login .facebook:hover { + background-color: transparent; + color: #335397; +} + +.login2 .login-wrapper .social-login .twitter { + background-color: #00c7f7; + border-color: #00c7f7; +} + +.login2 .login-wrapper .social-login .twitter:hover { + background-color: transparent; + color: #00c7f7; +} diff --git a/res/auth-ldap/views/index.jade b/res/auth-ldap/views/index.jade new file mode 100644 index 00000000..587c23e5 --- /dev/null +++ b/res/auth-ldap/views/index.jade @@ -0,0 +1,9 @@ +doctype html +html + head + title STF + meta(charset='utf-8') + include partials/styles + body(ng-cloak) + div(ng-view) + script(src='/static/lib/requirejs/require.js', data-main='static/scripts/main.js') diff --git a/res/auth-ldap/views/partials/signin.jade b/res/auth-ldap/views/partials/signin.jade new file mode 100644 index 00000000..d3fda513 --- /dev/null +++ b/res/auth-ldap/views/partials/signin.jade @@ -0,0 +1,28 @@ +.login2 + .login-wrapper + a(href='./') + img(width='128', height='128', src='/static/images/logo-128.png', title='STF') + + form(name='signin', novalidate, ng-submit='submit()') + .alert.alert-danger(ng-show='error') + span(ng-show='error.$invalid') Check errors below + span(ng-show='error.$incorrect') Incorrect login details + span(ng-show='error.$system') System error + + .form-group + .input-group + span.input-group-addon + i.fa.fa-envelope + input.form-control(ng-model='username', name='username', required, type='text', placeholder='LDAP Username') + .alert.alert-warning(ng-show='signin.username.$dirty && signin.username.$invalid') + span(ng-show='signin.username.$error.required') Please enter your LDAP username + + .form-group + .input-group + span.input-group-addon + i.fa.fa-lock + input.form-control(ng-model='password', name='password', required, type='password', placeholder='Password') + .alert.alert-warning(ng-show='signin.password.$dirty && signin.password.$invalid') + span Please enter your password + + input.btn.btn-lg.btn-primary.btn-block(type='submit', value='Log in') diff --git a/res/auth-ldap/views/partials/styles.jade b/res/auth-ldap/views/partials/styles.jade new file mode 100644 index 00000000..9b52edd2 --- /dev/null +++ b/res/auth-ldap/views/partials/styles.jade @@ -0,0 +1,6 @@ +link(href='http://fonts.googleapis.com/css?family=Lato:100,300,400,700', media='all', rel='stylesheet', type='text/css') +link(rel='stylesheet', href='/static/lib/se7en-bootstrap-3/build/stylesheets/bootstrap.min.css') +link(rel='stylesheet', href='/static/lib/se7en-bootstrap-3/build/stylesheets/se7en-font.css') +link(rel='stylesheet', href='/static/lib/se7en-bootstrap-3/build/stylesheets/style.css') +link(rel='stylesheet', href='/static/lib/se7en-bootstrap-3/build/stylesheets/font-awesome.min.css') +link(rel='stylesheet', href='/static/styles/login.css')