Implement FIDO u2f authentication

pull/7/head
Clement Michaud 2017-01-21 17:41:06 +01:00
parent 8c743228bf
commit 9670b23a8b
41 changed files with 2187 additions and 663 deletions

View File

@ -10,7 +10,7 @@ script:
- docker-compose build
- docker-compose up -d
- sleep 5
- npm run-script integration-test
- npm run-script int-test
deploy:
provider: npm
email: clement.michaud34@gmail.com

View File

@ -0,0 +1,8 @@
version: '2'
services:
auth:
volumes:
- ./src/views:/usr/src/views
- ./src/public_html:/usr/src/public_html

View File

@ -7,8 +7,8 @@ services:
- LDAP_URL=ldap://ldap
- LDAP_USERS_DN=dc=example,dc=com
- TOTP_SECRET=GRWGIJS6IRHVEODVNRCXCOBMJ5AGC6ZE
- JWT_SECRET=unsecure_secret
- JWT_EXPIRATION_TIME=1h
- SESSION_SECRET=unsecure_secret
- SESSION_EXPIRATION_TIME=3600000
depends_on:
- ldap
restart: always
@ -28,7 +28,8 @@ services:
- ./nginx_conf/nginx.conf:/etc/nginx/nginx.conf
- ./nginx_conf/index.html:/usr/share/nginx/html/index.html
- ./nginx_conf/secret.html:/usr/share/nginx/html/secret.html
- ./nginx_conf/ssl:/etc/ssl
depends_on:
- auth
ports:
- "8080:80"
- "8080:443"

View File

@ -16,7 +16,6 @@ worker_processes 1;
#pid logs/nginx.pid;
events {
worker_connections 1024;
}
@ -24,22 +23,28 @@ events {
http {
server {
listen 80;
listen 443 ssl;
root /usr/share/nginx/html;
server_name 127.0.0.1 localhost;
ssl on;
ssl_certificate /etc/ssl/server.crt;
ssl_certificate_key /etc/ssl/server.key;
error_page 401 = @error401;
location @error401 {
return 302 http://localhost:8080/auth/login?redirect=$request_uri;
return 302 https://localhost:8080/auth/login?redirect=$request_uri;
}
location = /check-auth {
location = /verify {
internal;
# proxy_pass_request_body off;
proxy_set_header X-Original-URI $request_uri;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_pass http://auth/_auth;
proxy_pass http://auth/_verify;
}
location /auth/ {
@ -51,7 +56,7 @@ http {
}
location = /secret.html {
auth_request /check-auth;
auth_request /verify;
auth_request_set $user $upstream_http_x_remote_user;
proxy_set_header X-Forwarded-User $user;
@ -60,14 +65,5 @@ http {
auth_request_set $expiry $upstream_http_remote_expiry;
proxy_set_header Remote-Expiry $expiry;
}
# Block everything but POST on _auth
location = /_auth {
if ($request_method != POST) {
return 403;
}
proxy_pass http://auth/_auth;
}
}
}

View File

@ -0,0 +1,13 @@
-----BEGIN CERTIFICATE-----
MIICATCCAWoCCQCvH2RvyOshNzANBgkqhkiG9w0BAQsFADBFMQswCQYDVQQGEwJB
VTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0
cyBQdHkgTHRkMB4XDTE3MDExNzIzMTc0M1oXDTE4MDExNzIzMTc0M1owRTELMAkG
A1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0
IFdpZGdpdHMgUHR5IEx0ZDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAzZaE
4XE1QyFNbrHBHRhSA53anAsJ5mBeG7Om6SdQcZAYahlDWEbtdoY4hy0gPNGcITcW
eE+WA+PvNRr7PczKEhneIyUUgV+nrz010fM5JnECPxLTe1oFzl4U8dyYiBpTziNz
hiUfq733PRYjcd9BQtcKcN4LdmQvjUHnnQ73TysCAwEAATANBgkqhkiG9w0BAQsF
AAOBgQAUFICtbuqXgL4HBRAg7yGbwokoH8Ar1QKZGe+F2WTR8vaDLOYUL7VsltLE
EJIGrcfs31nItHOBcLJuflrS8y0CQqes5puRw33LL2usSvO8z2q7JhCx+DSBi6yN
RbhcrGOllIdjsrbmd/zAMBVTUyxSisq3Nmk1cZayDvKg+GSAEA==
-----END CERTIFICATE-----

View File

@ -0,0 +1,11 @@
-----BEGIN CERTIFICATE REQUEST-----
MIIBhDCB7gIBADBFMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEh
MB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIGfMA0GCSqGSIb3DQEB
AQUAA4GNADCBiQKBgQDNloThcTVDIU1uscEdGFIDndqcCwnmYF4bs6bpJ1BxkBhq
GUNYRu12hjiHLSA80ZwhNxZ4T5YD4+81Gvs9zMoSGd4jJRSBX6evPTXR8zkmcQI/
EtN7WgXOXhTx3JiIGlPOI3OGJR+rvfc9FiNx30FC1wpw3gt2ZC+NQeedDvdPKwID
AQABoAAwDQYJKoZIhvcNAQELBQADgYEAmCX60kspIw1Zfb79AQOarFW5Q2K2h5Vx
/cRbDyHlKtbmG77EtICccULyqf76B1gNRw5Zq3lSotSUcLzsWcdesXCFDC7k87Qf
mpQKPj6GdTYJvdWf8aDwt32tAqWuBIRoAbdx5WbFPPWVfDcm7zDJefBrhNUDH0Qd
vcnxjvPMmOM=
-----END CERTIFICATE REQUEST-----

View File

@ -0,0 +1,15 @@
-----BEGIN RSA PRIVATE KEY-----
MIICXQIBAAKBgQDNloThcTVDIU1uscEdGFIDndqcCwnmYF4bs6bpJ1BxkBhqGUNY
Ru12hjiHLSA80ZwhNxZ4T5YD4+81Gvs9zMoSGd4jJRSBX6evPTXR8zkmcQI/EtN7
WgXOXhTx3JiIGlPOI3OGJR+rvfc9FiNx30FC1wpw3gt2ZC+NQeedDvdPKwIDAQAB
AoGBAIwGcfkO30UawJ+daDeF4g5ejI/toM+NYWuiwBNbWJoQl+Bj1o+gt4obvxKq
tKNX7OxelepZ4oZB0CIuf2LHQfU6cVGdu//or7nfS2FLBYStopZyL6KorZbkqsj1
ikQN4GosJQqaYkexnwjItMFaHaRRX6YnIXp42Jl1glitO3+5AkEA+thn/vwFo24I
fC+7ORpmLi+BVAkTuhMm+C6TIV6s64B+A5oQ82OBCYK9YCOWmS6JHHFDrxJla+3M
2U9KXky63wJBANHQCFCirfuT6esSjbqpCeqtmZG5LWHtL12V9DF7yjHPjmHL9uRu
e9W+Uz33IJbqd82gtZ/ARfpYEjD0JEieQTUCQFo872xzDTQ1qSfDo/5u2MNUo5mv
ikEuEp7FYnhmrp4poyt4iRCFgy4Ask+bfdmtO/XXaRnZ7FJfQYoLVB2ITNECQQCN
gOiauZztl4yj5heAVJFDnWF9To61BOp1C7VtyjdL8NfuTUluNrV+KqapnAp2vhue
q0zTOTH47X0XVxFBiLohAkBuQzPey5I3Ui8inE4sDt/fqX8r/GMhBTxIb9KlV/H6
jKZNs/83n5/ohaX36er8svW9PB4pcqENZ+kBpvDtKVwS
-----END RSA PRIVATE KEY-----

View File

@ -5,7 +5,9 @@
"main": "src/index.js",
"scripts": {
"test": "./node_modules/.bin/mocha --recursive test/unitary",
"integration-test": "./node_modules/.bin/mocha --recursive test/integration",
"unit-test": "./node_modules/.bin/mocha --recursive test/unitary",
"int-test": "./node_modules/.bin/mocha --recursive test/integration",
"all-test": "./node_modules/.bin/mocha --recursive test",
"coverage": "./node_modules/.bin/istanbul cover _mocha -- -R spec"
},
"repository": {
@ -18,16 +20,16 @@
"url": "https://github.com/clems4ever/two-factor-auth-server/issues"
},
"dependencies": {
"authdog": "^0.1.1",
"bluebird": "^3.4.7",
"body-parser": "^1.15.2",
"cookie-parser": "^1.4.3",
"ejs": "^2.5.5",
"express": "^4.14.0",
"jsonwebtoken": "^7.2.1",
"express-session": "^1.14.2",
"ldapjs": "^1.0.1",
"object-path": "^0.11.3",
"q": "^1.4.1",
"speakeasy": "^2.0.0"
"speakeasy": "^2.0.0",
"winston": "^2.3.1"
},
"devDependencies": {
"mocha": "^3.2.0",

View File

@ -2,14 +2,15 @@
var server = require('./lib/server');
var ldap = require('ldapjs');
var u2f = require('authdog');
var config = {
port: process.env.PORT || 8080,
totp_secret: process.env.TOTP_SECRET,
ldap_url: process.env.LDAP_URL || 'ldap://127.0.0.1:389',
ldap_users_dn: process.env.LDAP_USERS_DN,
jwt_secret: process.env.JWT_SECRET,
jwt_expiration_time: process.env.JWT_EXPIRATION_TIME || '1h'
session_secret: process.env.SESSION_SECRET,
session_max_age: process.env.SESSION_MAX_AGE || 3600000 // in ms
}
var ldap_client = ldap.createClient({
@ -17,4 +18,4 @@ var ldap_client = ldap.createClient({
reconnect: true
});
server.run(config, ldap_client);
server.run(config, ldap_client, u2f);

View File

@ -1,19 +0,0 @@
module.exports = {
verify: verify_authentication
}
var objectPath = require('object-path');
var utils = require('./utils');
function verify_authentication(req, res) {
console.log('Verify authentication');
if(!objectPath.has(req, 'cookies.access_token')) {
return utils.reject('No access token provided');
}
var jsonWebToken = req.cookies['access_token'];
return req.app.get('jwt engine').verify(jsonWebToken);
}

View File

@ -1,32 +0,0 @@
module.exports = Jwt;
var jwt = require('jsonwebtoken');
var utils = require('./utils');
var Promise = require('bluebird');
function Jwt(secret) {
this._secret = secret;
}
Jwt.prototype.sign = function(data, expiration_time) {
var that = this;
return new Promise(function(resolve, reject) {
var token = jwt.sign(data, that._secret, { expiresIn: expiration_time })
resolve(token);
});
}
Jwt.prototype.verify = function(token) {
var that = this;
return new Promise(function(resolve, reject) {
try {
var decoded = jwt.verify(token, that._secret);
resolve(decoded);
}
catch(err) {
reject(err.message);
}
});
}

View File

@ -7,7 +7,7 @@ var util = require('util');
var Promise = require('bluebird');
function validateCredentials(ldap_client, username, password, users_dn) {
var userDN = util.format("binding entry cn=%s,%s", username, users_dn);
var bind_promised = Promise.promisify(ldap_client.bind, ldap_client);
var userDN = util.format("cn=%s,%s", username, users_dn);
var bind_promised = Promise.promisify(ldap_client.bind, { context: ldap_client });
return bind_promised(userDN, password);
}

View File

@ -1,27 +0,0 @@
module.exports = {
'authentication_failed': authentication_failed,
'authentication_succeeded': authentication_succeeded,
'already_authenticated': already_authenticated
}
function authentication_failed(res) {
console.log('Reply: authentication failed');
res.status(401)
res.send('Authentication failed');
}
function authentication_succeeded(res, username, token) {
console.log('Reply: authentication succeeded');
res.status(200);
res.set({ 'X-Remote-User': username });
res.send(token);
}
function already_authenticated(res, username) {
console.log('Reply: already authenticated');
res.status(204);
res.set({ 'X-Remote-User': username });
res.send();
}

View File

@ -1,39 +1,31 @@
var first_factor = require('./routes/first_factor');
var second_factor = require('./routes/second_factor');
var verify = require('./routes/verify');
module.exports = {
auth: serveAuth,
login: serveLogin,
logout: serveLogout,
first_factor: first_factor
}
var authentication = require('./authentication');
var replies = require('./replies');
function serveAuth(req, res) {
serveAuthGet(req, res);
}
function serveAuthGet(req, res) {
authentication.verify(req, res)
.then(function(user) {
replies.already_authenticated(res, user);
})
.catch(function(err) {
replies.authentication_failed(res);
console.error(err);
});
verify: verify,
first_factor: first_factor,
second_factor: second_factor
}
function serveLogin(req, res) {
req.session.auth_session = {};
req.session.auth_session.first_factor = false;
req.session.auth_session.second_factor = false;
res.render('login');
}
function serveLogout(req, res) {
var redirect_param = req.query.redirect;
var redirect_url = redirect_param || '/';
res.clearCookie('access_token');
req.session.auth_session = {
first_factor: false,
second_factor: false
}
res.redirect(redirect_url);
}

View File

@ -0,0 +1,24 @@
module.exports = denyNotLogged;
var objectPath = require('object-path');
function replyWithUnauthorized(res) {
res.status(401);
res.send('Unauthorized access');
}
function denyNotLogged(next) {
return function(req, res) {
var auth_session = req.session.auth_session;
var first_factor = objectPath.has(req, 'session.auth_session.first_factor')
&& req.session.auth_session.first_factor;
if(!first_factor) {
replyWithUnauthorized(res);
console.log('Access to this route is denied');
return;
}
next(req, res);
}
}

View File

@ -2,14 +2,24 @@
module.exports = first_factor;
var ldap = require('../ldap');
var objectPath = require('object-path');
function replyWithUnauthorized(res) {
res.status(401);
res.send();
}
function first_factor(req, res) {
if(!objectPath.has(req, 'session.auth_session.second_factor')) {
replyWithUnauthorized(res);
}
var username = req.body.username;
var password = req.body.password;
console.log('Start authentication of user %s', username);
if(!username || !password) {
replies.authentication_failed(res);
replyWithUnauthorized(res);
return;
}
@ -18,13 +28,14 @@ function first_factor(req, res) {
ldap.validate(ldap_client, username, password, config.ldap_users_dn)
.then(function() {
req.session.auth_session.userid = username;
req.session.auth_session.first_factor = true;
res.status(204);
res.send();
console.log('LDAP binding successful');
})
.error(function(err) {
res.status(401);
res.send();
.catch(function(err) {
replyWithUnauthorized(res);
console.log('LDAP binding failed:', err);
});
}

View File

@ -0,0 +1,16 @@
var user_key_container = {};
var denyNotLogged = require('./deny_not_logged');
var u2f = require('./u2f')(user_key_container); // create a u2f handler bound to
// user key container
module.exports = {
totp: denyNotLogged(require('./totp')),
u2f: {
register_request: denyNotLogged(u2f.register_request),
register: denyNotLogged(u2f.register),
sign_request: denyNotLogged(u2f.sign_request),
sign: denyNotLogged(u2f.sign),
}
}

View File

@ -0,0 +1,33 @@
module.exports = totp;
var totp = require('../totp');
var objectPath = require('object-path');
var UNAUTHORIZED_MESSAGE = 'Unauthorized access';
function replyWithUnauthorized(res) {
res.status(401);
res.send();
}
function totp(req, res) {
if(!objectPath.has(req, 'session.auth_session.second_factor')) {
replyWithUnauthorized(res);
}
var token = req.body.token;
var totp_engine = req.app.get('totp engine');
var config = req.app.get('config');
totp.validate(totp_engine, token, config.totp_secret)
.then(function() {
req.session.auth_session.second_factor = true;
res.status(204);
res.send();
})
.catch(function(err) {
console.error(err);
replyWithUnauthorized(res);
});
}

View File

@ -0,0 +1,144 @@
module.exports = function(user_key_container) {
return {
register_request: register_request,
register: register(user_key_container),
sign_request: sign_request(user_key_container),
sign: sign(user_key_container),
}
}
var objectPath = require('object-path');
var util = require('util');
function replyWithInternalError(res, msg) {
res.status(500);
res.send(msg)
}
function replyWithMissingRegistration(res) {
res.status(401);
res.send('Please register before authenticate');
}
function replyWithUnauthorized(res) {
res.status(401);
res.send();
}
function register_request(req, res) {
var u2f = req.app.get('u2f');
var logger = req.app.get('logger');
var app_id = util.format('https://%s', req.headers.host);
logger.debug('U2F register_request: headers=%s', JSON.stringify(req.headers));
logger.info('U2F register_request: Starting registration');
u2f.startRegistration(app_id, [])
.then(function(registrationRequest) {
logger.info('U2F register_request: Sending back registration request');
req.session.auth_session.register_request = registrationRequest;
res.status(200);
res.json(registrationRequest);
}, function(err) {
logger.error('U2F register_request: %s', err);
replyWithInternalError(res, 'Unable to complete the registration');
});
}
function register(user_key_container) {
return function(req, res) {
if(!objectPath.has(req, 'session.auth_session.register_request')) {
replyWithUnauthorized(res);
return;
}
var u2f = req.app.get('u2f');
var registrationRequest = req.session.auth_session.register_request;
var logger = req.app.get('logger');
logger.info('U2F register: Finishing registration');
logger.debug('U2F register: register_request=%s', JSON.stringify(registrationRequest));
logger.debug('U2F register: body=%s', JSON.stringify(req.body));
u2f.finishRegistration(registrationRequest, req.body)
.then(function(registrationStatus) {
logger.info('U2F register: Store registration and reply');
var meta = {
keyHandle: registrationStatus.keyHandle,
publicKey: registrationStatus.publicKey,
certificate: registrationStatus.certificate
}
user_key_container[req.session.auth_session.userid] = meta;
res.status(204);
res.send();
}, function(err) {
logger.error('U2F register: %s', err);
replyWithInternalError(res, 'Unable to complete the registration');
});
}
}
function userKeyExists(req, user_key_container) {
return req.session.auth_session.userid in user_key_container;
}
function sign_request(user_key_container) {
return function(req, res) {
if(!userKeyExists(req, user_key_container)) {
replyWithMissingRegistration(res);
return;
}
var logger = req.app.get('logger');
var u2f = req.app.get('u2f');
var key = user_key_container[req.session.auth_session.userid];
var app_id = util.format('https://%s', req.headers.host);
logger.info('U2F sign_request: Start authentication');
u2f.startAuthentication(app_id, [key])
.then(function(authRequest) {
logger.info('U2F sign_request: Store authentication request and reply');
req.session.auth_session.sign_request = authRequest;
res.status(200);
res.json(authRequest);
}, function(err) {
logger.info('U2F sign_request: %s', err);
replyWithUnauthorized(res);
});
}
}
function sign(user_key_container) {
return function(req, res) {
if(!userKeyExists(req, user_key_container)) {
replyWithMissingRegistration(res);
return;
}
if(!objectPath.has(req, 'session.auth_session.sign_request')) {
replyWithUnauthorized(res);
return;
}
var logger = req.app.get('logger');
var u2f = req.app.get('u2f');
var authRequest = req.session.auth_session.sign_request;
var key = user_key_container[req.session.auth_session.userid];
logger.info('U2F sign: Finish authentication');
u2f.finishAuthentication(authRequest, req.body, [key])
.then(function(authenticationStatus) {
logger.info('U2F sign: Authentication successful');
req.session.auth_session.second_factor = true;
res.status(204);
res.send();
}, function(err) {
logger.error('U2F sign: %s', err);
res.status(401);
res.send();
});
}
}

View File

@ -0,0 +1,37 @@
module.exports = verify;
var objectPath = require('object-path');
var Promise = require('bluebird');
function verify_filter(req, res) {
if(!objectPath.has(req, 'session.auth_session'))
return Promise.reject('No auth_session variable');
if(!objectPath.has(req, 'session.auth_session.first_factor'))
return Promise.reject('No first factor variable');
if(!objectPath.has(req, 'session.auth_session.second_factor'))
return Promise.reject('No second factor variable');
if(!req.session.auth_session.first_factor ||
!req.session.auth_session.second_factor)
return Promise.reject('First or second factor not validated');
return Promise.resolve();
}
function verify(req, res) {
console.log('Verify authentication');
verify_filter(req, res)
.then(function() {
res.status(204);
res.send();
})
.catch(function(err) {
res.status(401);
res.send();
});
}

View File

@ -4,39 +4,60 @@ module.exports = {
}
var routes = require('./routes');
var Jwt = require('./jwt');
var express = require('express');
var bodyParser = require('body-parser');
var cookieParser = require('cookie-parser');
var speakeasy = require('speakeasy');
var path = require('path');
var session = require('express-session');
var winston = require('winston');
function run(config, ldap_client) {
function run(config, ldap_client, u2f, fn) {
var view_directory = path.resolve(__dirname, '../views');
var public_html_directory = path.resolve(__dirname, '../public_html');
var app = express();
app.use(cookieParser());
app.use(express.static(public_html_directory));
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
app.set('trust proxy', 1); // trust first proxy
app.use(session({
secret: config.session_secret,
resave: false,
saveUninitialized: true,
cookie: {
secure: false,
maxAge: config.session_max_age
},
}));
app.set('views', view_directory);
app.set('view engine', 'ejs');
app.set('jwt engine', new Jwt(config.jwt_secret));
winston.level = 'debug';
app.set('logger', winston);
app.set('ldap client', ldap_client);
app.set('totp engine', speakeasy);
app.set('u2f', u2f);
app.set('config', config);
app.get ('/login', routes.login);
app.get ('/logout', routes.logout);
app.get ('/_auth', routes.auth);
app.get ('/_verify', routes.verify);
app.post ('/_auth/1stfactor', routes.first_factor);
app.post ('/_auth/1stfactor', routes.first_factor);
app.post ('/_auth/2ndfactor/totp', routes.second_factor.totp);
app.get ('/_auth/2ndfactor/u2f/register_request', routes.second_factor.u2f.register_request);
app.post ('/_auth/2ndfactor/u2f/register', routes.second_factor.u2f.register);
app.get ('/_auth/2ndfactor/u2f/sign_request', routes.second_factor.u2f.sign_request);
app.post ('/_auth/2ndfactor/u2f/sign', routes.second_factor.u2f.sign);
app.listen(config.port, function(err) {
return app.listen(config.port, function(err) {
console.log('Listening on %d...', config.port);
if(fn) fn();
});
}

View File

@ -3,20 +3,20 @@ module.exports = {
'validate': validate
}
var Q = require('q');
var Promise = require('bluebird');
function validate(totp_engine, token, totp_secret) {
var defer = Q.defer();
var real_token = totp_engine.totp({
secret: totp_secret,
encoding: 'base32'
});
return new Promise(function(resolve, reject) {
var real_token = totp_engine.totp({
secret: totp_secret,
encoding: 'base32'
});
if(token == real_token) {
defer.resolve();
}
else {
defer.reject('Wrong challenge');
}
return defer.promise;
if(token == real_token) {
resolve();
}
else {
reject('Wrong challenge');
}
});
}

View File

@ -1,2 +0,0 @@
/*! js-cookie v2.1.3 | MIT */
!function(a){var b=!1;if("function"==typeof define&&define.amd&&(define(a),b=!0),"object"==typeof exports&&(module.exports=a(),b=!0),!b){var c=window.Cookies,d=window.Cookies=a();d.noConflict=function(){return window.Cookies=c,d}}}(function(){function a(){for(var a=0,b={};a<arguments.length;a++){var c=arguments[a];for(var d in c)b[d]=c[d]}return b}function b(c){function d(b,e,f){var g;if("undefined"!=typeof document){if(arguments.length>1){if(f=a({path:"/"},d.defaults,f),"number"==typeof f.expires){var h=new Date;h.setMilliseconds(h.getMilliseconds()+864e5*f.expires),f.expires=h}try{g=JSON.stringify(e),/^[\{\[]/.test(g)&&(e=g)}catch(i){}return e=c.write?c.write(e,b):encodeURIComponent(e+"").replace(/%(23|24|26|2B|3A|3C|3E|3D|2F|3F|40|5B|5D|5E|60|7B|7D|7C)/g,decodeURIComponent),b=encodeURIComponent(b+""),b=b.replace(/%(23|24|26|2B|5E|60|7C)/g,decodeURIComponent),b=b.replace(/[\(\)]/g,escape),document.cookie=b+"="+e+(f.expires?"; expires="+f.expires.toUTCString():"")+(f.path?"; path="+f.path:"")+(f.domain?"; domain="+f.domain:"")+(f.secure?"; secure":"")}b||(g={});for(var j=document.cookie?document.cookie.split("; "):[],k=/(%[0-9A-Z]{2})+/g,l=0;l<j.length;l++){var m=j[l].split("="),n=m.slice(1).join("=");'"'===n.charAt(0)&&(n=n.slice(1,-1));try{var o=m[0].replace(k,decodeURIComponent);if(n=c.read?c.read(n,o):c(n,o)||n.replace(k,decodeURIComponent),this.json)try{n=JSON.parse(n)}catch(i){}if(b===o){g=n;break}b||(g[o]=n)}catch(i){}}return g}}return d.set=d,d.get=function(a){return d.call(d,a)},d.getJSON=function(){return d.apply({json:!0},[].slice.call(arguments))},d.defaults={},d.remove=function(b,c){d(b,"",a(c,{expires:-1}))},d.withConverter=b,d}return b(function(){})});

View File

@ -1,4 +1,4 @@
@import url(http://fonts.googleapis.com/css?family=Open+Sans);
@import url(https://fonts.googleapis.com/css?family=Open+Sans);
.btn { display: inline-block; *display: inline; *zoom: 1; padding: 4px 10px 4px; margin-bottom: 0; font-size: 13px; line-height: 18px; color: #333333; text-align: center;text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75); vertical-align: middle; background-color: #f5f5f5; background-image: -moz-linear-gradient(top, #ffffff, #e6e6e6); background-image: -ms-linear-gradient(top, #ffffff, #e6e6e6); background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#e6e6e6)); background-image: -webkit-linear-gradient(top, #ffffff, #e6e6e6); background-image: -o-linear-gradient(top, #ffffff, #e6e6e6); background-image: linear-gradient(top, #ffffff, #e6e6e6); background-repeat: repeat-x; filter: progid:dximagetransform.microsoft.gradient(startColorstr=#ffffff, endColorstr=#e6e6e6, GradientType=0); border-color: #e6e6e6 #e6e6e6 #e6e6e6; border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); border: 1px solid #e6e6e6; -webkit-border-radius: 4px; -moz-border-radius: 4px; border-radius: 4px; -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); cursor: pointer; *margin-left: .3em; }
.btn:hover, .btn:active, .btn.active, .btn.disabled, .btn[disabled] { background-color: #e6e6e6; }
.btn-large { padding: 9px 14px; font-size: 15px; line-height: normal; -webkit-border-radius: 5px; -moz-border-radius: 5px; border-radius: 5px; }
@ -25,6 +25,12 @@ body {
background: -webkit-radial-gradient(0% 100%, ellipse cover, rgba(104,128,138,.4) 10%,rgba(138,114,76,0) 40%), linear-gradient(to bottom, rgba(57,173,219,.25) 0%,rgba(42,60,87,.4) 100%), linear-gradient(135deg, #670d10 0%,#092756 100%);
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#3E1D6D', endColorstr='#092756',GradientType=1 );
}
.vr {
margin-left: 10px;
margin-right: 10px;
}
.login {
position: absolute;
top: 50%;
@ -35,6 +41,8 @@ body {
}
.login h1 { color: #fff; text-shadow: 0 0 10px rgba(0,0,0,0.3); letter-spacing:1px; text-align:center; }
.login h2 { color: #fff; text-shadow: 0 0 10px rgba(0,0,0,0.3); letter-spacing:1px; text-align:center; font-size: 1em; }
input {
width: 100%;
margin-bottom: 10px;
@ -71,3 +79,25 @@ input:focus { box-shadow: inset 0 -5px 45px rgba(100,100,100,0.4), 0 1px 1px rgb
#information.success {
background-color: rgb(43, 188, 99);
}
#second-factor {
width: 400px;
}
#second-factor .login {
display: inline-block;
}
#second-factor #totp {
width: 180px;
float: left;
}
#second-factor #u2f {
width: 180px;
float: right;
}
#second-factor #u2f button {
margin-top: 5px;
}

View File

@ -2,20 +2,14 @@
params={};
location.search.replace(/[?&]+([^=&]+)=([^&]*)/gi,function(s,k,v){params[k]=v});
console.log(params);
$(document).ready(function() {
$('#login-button').on('click', onLoginButtonClicked);
setupEnterKeypressListener();
$('#information').hide();
});
function setupEnterKeypressListener() {
$('#login-form').on('keydown', 'input', function (e) {
function setupEnterKeypressListener(filter, fn) {
$(filter).on('keydown', 'input', function (e) {
var key = e.which;
switch (key) {
case 13: // enter key code
onLoginButtonClicked();
fn();
break;
default:
break;
@ -26,50 +20,144 @@ function setupEnterKeypressListener() {
function onLoginButtonClicked() {
var username = $('#username').val();
var password = $('#password').val();
var token = $('#token').val();
authenticate(username, password, token, function(err, access_token) {
validateFirstFactor(username, password, function(err) {
if(err) {
onAuthenticationFailure();
onFirstFactorFailure();
return;
}
onAuthenticationSuccess(access_token);
onFirstFactorSuccess();
});
}
function onTotpSignButtonClicked() {
var token = $('#totp-token').val();
validateSecondFactorTotp(token, function(err) {
if(err) {
onSecondFactorTotpFailure();
return;
}
onSecondFactorTotpSuccess();
});
}
function authenticate(username, password, token, fn) {
$.post('/_auth', {
username: username,
password: password,
token: token
function onU2fSignButtonClicked() {
startSecondFactorU2fSigning(function(err) {
if(err) {
onSecondFactorU2fSigningFailure();
return;
}
onSecondFactorU2fSigningSuccess();
}, 120);
}
function onU2fRegisterButtonClicked() {
startSecondFactorU2fRegister(function(err) {
if(err) {
onSecondFactorU2fRegisterFailure();
return;
}
onSecondFactorU2fRegisterSuccess();
}, 120);
}
function finishSecondFactorU2f(url, responseData, fn) {
console.log(responseData);
$.ajax({
type: 'POST',
url: url,
data: JSON.stringify(responseData),
contentType: 'application/json',
dataType: 'json',
})
.done(function(access_token) {
fn(undefined, access_token);
.done(function(data) {
fn(undefined, data);
})
.fail(function(xhr, status) {
$.notify('Error when finish U2F transaction' + status);
});
}
function startSecondFactorU2fSigning(fn, timeout) {
$.get('/auth/_auth/2ndfactor/u2f/sign_request', {}, null, 'json')
.done(function(signResponse) {
var registeredKeys = signResponse.registeredKeys;
$.notify('Please touch the token', 'information');
console.log(signResponse);
// Store sessionIds
// var sessionIds = {};
// for (var i = 0; i < registeredKeys.length; i++) {
// sessionIds[registeredKeys[i].keyHandle] = registeredKeys[i].sessionId;
// delete registeredKeys[i]['sessionId'];
// }
u2f.sign(
signResponse.appId,
signResponse.challenge,
signResponse.registeredKeys,
function (response) {
if (response.errorCode) {
fn(response);
} else {
// response['sessionId'] = sessionIds[response.keyHandle];
finishSecondFactorU2f('/auth/_auth/2ndfactor/u2f/sign', response, fn);
}
},
timeout
);
})
.fail(function(xhr, status) {
fn(status);
});
}
function startSecondFactorU2fRegister(fn, timeout) {
$.get('/auth/_auth/2ndfactor/u2f/register_request', {}, null, 'json')
.done(function(startRegisterResponse) {
console.log(startRegisterResponse);
$.notify('Please touch the token', 'information');
u2f.register(
startRegisterResponse.appId,
startRegisterResponse.registerRequests,
startRegisterResponse.registeredKeys,
function (response) {
if (response.errorCode) {
fn(response.errorCode);
} else {
// response['sessionId'] = startRegisterResponse.clientData;
finishSecondFactorU2f('/auth/_auth/2ndfactor/u2f/register', response, fn);
}
},
timeout
);
});
}
function validateSecondFactorTotp(token, fn) {
$.post('/auth/_auth/2ndfactor/totp', {
token: token,
})
.done(function() {
fn(undefined);
})
.fail(function(err) {
fn(err);
});
}
function displayInformationMessage(msg, type, time, fn) {
if(type == 'success') {
$('#information').addClass("success");
}
else if(type == 'failure') {
$('#information').addClass("failure");
}
$('#information').text(msg);
$('#information').show("fast");
setTimeout(function() {
$('#information').hide("fast");
$('#information').removeClass("success");
$('#information').removeClass("failure");
if(fn) fn();
},time);
function validateFirstFactor(username, password, fn) {
$.post('/auth/_auth/1stfactor', {
username: username,
password: password,
})
.done(function() {
fn(undefined);
})
.fail(function(err) {
fn(err);
});
}
function redirect() {
@ -77,28 +165,112 @@ function redirect() {
if('redirect' in params) {
redirect_uri = params['redirect'];
}
window.location.replace(redirect_uri);
}
function onAuthenticationSuccess(access_token) {
Cookies.set('access_token', access_token, { path: '/' });
function onFirstFactorSuccess() {
$('#username').val('');
$('#password').val('');
$('#token').val('');
redirect();
// displayInformationMessage('Authentication success, You will be redirected' +
// 'in few seconds.', 'success', 3000, function() {
// });
enterSecondFactor();
}
function onAuthenticationFailure() {
function onFirstFactorFailure() {
$('#password').val('');
$('#token').val('');
displayInformationMessage('Authentication failed, please try again.', 'failure', 3000);
$.notify('Wrong credentials', 'error');
}
function onAuthenticationSuccess() {
$.notify('Authentication succeeded. You are redirected.', 'success');
redirect();
}
function onSecondFactorTotpSuccess() {
onAuthenticationSuccess();
}
function onSecondFactorTotpFailure() {
$.notify('Wrong TOTP token', 'error');
}
function onSecondFactorU2fSigningSuccess() {
onAuthenticationSuccess();
}
function onSecondFactorU2fSigningFailure(err) {
console.error(err);
$.notify('Problem authenticating with U2F.', 'error');
}
function onSecondFactorU2fRegisterSuccess() {
$.notify('Registration succeeded. You can now sign in.', 'success');
}
function onSecondFactorU2fRegisterFailure(err) {
console.error(err);
$.notify('Problem authenticating with U2F.', 'error');
}
function showFirstFactorLayout() {
$('#first-factor').show();
}
function hideFirstFactorLayout() {
$('#first-factor').hide();
}
function showSecondFactorLayout() {
$('#second-factor').show();
}
function hideSecondFactorLayout() {
$('#second-factor').hide();
}
function setupFirstFactorLoginButton() {
$('#first-factor #login-button').on('click', onLoginButtonClicked);
setupEnterKeypressListener('#login-form', onLoginButtonClicked);
$('#first-factor #information').hide();
}
function cleanupFirstFactorLoginButton() {
$('#first-factor #login-button').off('click');
}
function setupTotpSignButton() {
$('#second-factor #totp-sign-button').on('click', onTotpSignButtonClicked);
setupEnterKeypressListener('#totp', onTotpSignButtonClicked);
}
function setupU2fSignButton() {
$('#second-factor #u2f-sign-button').on('click', onU2fSignButtonClicked);
setupEnterKeypressListener('#u2f', onU2fSignButtonClicked);
}
function setupU2fRegisterButton() {
$('#second-factor #u2f-register-button').on('click', onU2fRegisterButtonClicked);
setupEnterKeypressListener('#u2f', onU2fRegisterButtonClicked);
}
function enterFirstFactor() {
// console.log('entering first factor');
showFirstFactorLayout();
hideSecondFactorLayout();
setupFirstFactorLoginButton();
}
function enterSecondFactor() {
// console.log('entering second factor');
hideFirstFactorLayout();
showSecondFactorLayout();
cleanupFirstFactorLoginButton();
setupTotpSignButton();
setupU2fSignButton();
setupU2fRegisterButton();
}
$(document).ready(function() {
enterFirstFactor();
});
})();

1
src/public_html/notify.min.js vendored 100644

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,748 @@
//Copyright 2014-2015 Google Inc. All rights reserved.
//Use of this source code is governed by a BSD-style
//license that can be found in the LICENSE file or at
//https://developers.google.com/open-source/licenses/bsd
/**
* @fileoverview The U2F api.
*/
'use strict';
/**
* Namespace for the U2F api.
* @type {Object}
*/
var u2f = u2f || {};
/**
* FIDO U2F Javascript API Version
* @number
*/
var js_api_version;
/**
* The U2F extension id
* @const {string}
*/
// The Chrome packaged app extension ID.
// Uncomment this if you want to deploy a server instance that uses
// the package Chrome app and does not require installing the U2F Chrome extension.
u2f.EXTENSION_ID = 'kmendfapggjehodndflmmgagdbamhnfd';
// The U2F Chrome extension ID.
// Uncomment this if you want to deploy a server instance that uses
// the U2F Chrome extension to authenticate.
// u2f.EXTENSION_ID = 'pfboblefjcgdjicmnffhdgionmgcdmne';
/**
* Message types for messsages to/from the extension
* @const
* @enum {string}
*/
u2f.MessageTypes = {
'U2F_REGISTER_REQUEST': 'u2f_register_request',
'U2F_REGISTER_RESPONSE': 'u2f_register_response',
'U2F_SIGN_REQUEST': 'u2f_sign_request',
'U2F_SIGN_RESPONSE': 'u2f_sign_response',
'U2F_GET_API_VERSION_REQUEST': 'u2f_get_api_version_request',
'U2F_GET_API_VERSION_RESPONSE': 'u2f_get_api_version_response'
};
/**
* Response status codes
* @const
* @enum {number}
*/
u2f.ErrorCodes = {
'OK': 0,
'OTHER_ERROR': 1,
'BAD_REQUEST': 2,
'CONFIGURATION_UNSUPPORTED': 3,
'DEVICE_INELIGIBLE': 4,
'TIMEOUT': 5
};
/**
* A message for registration requests
* @typedef {{
* type: u2f.MessageTypes,
* appId: ?string,
* timeoutSeconds: ?number,
* requestId: ?number
* }}
*/
u2f.U2fRequest;
/**
* A message for registration responses
* @typedef {{
* type: u2f.MessageTypes,
* responseData: (u2f.Error | u2f.RegisterResponse | u2f.SignResponse),
* requestId: ?number
* }}
*/
u2f.U2fResponse;
/**
* An error object for responses
* @typedef {{
* errorCode: u2f.ErrorCodes,
* errorMessage: ?string
* }}
*/
u2f.Error;
/**
* Data object for a single sign request.
* @typedef {enum {BLUETOOTH_RADIO, BLUETOOTH_LOW_ENERGY, USB, NFC}}
*/
u2f.Transport;
/**
* Data object for a single sign request.
* @typedef {Array<u2f.Transport>}
*/
u2f.Transports;
/**
* Data object for a single sign request.
* @typedef {{
* version: string,
* challenge: string,
* keyHandle: string,
* appId: string
* }}
*/
u2f.SignRequest;
/**
* Data object for a sign response.
* @typedef {{
* keyHandle: string,
* signatureData: string,
* clientData: string
* }}
*/
u2f.SignResponse;
/**
* Data object for a registration request.
* @typedef {{
* version: string,
* challenge: string
* }}
*/
u2f.RegisterRequest;
/**
* Data object for a registration response.
* @typedef {{
* version: string,
* keyHandle: string,
* transports: Transports,
* appId: string
* }}
*/
u2f.RegisterResponse;
/**
* Data object for a registered key.
* @typedef {{
* version: string,
* keyHandle: string,
* transports: ?Transports,
* appId: ?string
* }}
*/
u2f.RegisteredKey;
/**
* Data object for a get API register response.
* @typedef {{
* js_api_version: number
* }}
*/
u2f.GetJsApiVersionResponse;
//Low level MessagePort API support
/**
* Sets up a MessagePort to the U2F extension using the
* available mechanisms.
* @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback
*/
u2f.getMessagePort = function(callback) {
if (typeof chrome != 'undefined' && chrome.runtime) {
// The actual message here does not matter, but we need to get a reply
// for the callback to run. Thus, send an empty signature request
// in order to get a failure response.
var msg = {
type: u2f.MessageTypes.U2F_SIGN_REQUEST,
signRequests: []
};
chrome.runtime.sendMessage(u2f.EXTENSION_ID, msg, function() {
if (!chrome.runtime.lastError) {
// We are on a whitelisted origin and can talk directly
// with the extension.
u2f.getChromeRuntimePort_(callback);
} else {
// chrome.runtime was available, but we couldn't message
// the extension directly, use iframe
u2f.getIframePort_(callback);
}
});
} else if (u2f.isAndroidChrome_()) {
u2f.getAuthenticatorPort_(callback);
} else if (u2f.isIosChrome_()) {
u2f.getIosPort_(callback);
} else {
// chrome.runtime was not available at all, which is normal
// when this origin doesn't have access to any extensions.
u2f.getIframePort_(callback);
}
};
/**
* Detect chrome running on android based on the browser's useragent.
* @private
*/
u2f.isAndroidChrome_ = function() {
var userAgent = navigator.userAgent;
return userAgent.indexOf('Chrome') != -1 &&
userAgent.indexOf('Android') != -1;
};
/**
* Detect chrome running on iOS based on the browser's platform.
* @private
*/
u2f.isIosChrome_ = function() {
return $.inArray(navigator.platform, ["iPhone", "iPad", "iPod"]) > -1;
};
/**
* Connects directly to the extension via chrome.runtime.connect.
* @param {function(u2f.WrappedChromeRuntimePort_)} callback
* @private
*/
u2f.getChromeRuntimePort_ = function(callback) {
var port = chrome.runtime.connect(u2f.EXTENSION_ID,
{'includeTlsChannelId': true});
setTimeout(function() {
callback(new u2f.WrappedChromeRuntimePort_(port));
}, 0);
};
/**
* Return a 'port' abstraction to the Authenticator app.
* @param {function(u2f.WrappedAuthenticatorPort_)} callback
* @private
*/
u2f.getAuthenticatorPort_ = function(callback) {
setTimeout(function() {
callback(new u2f.WrappedAuthenticatorPort_());
}, 0);
};
/**
* Return a 'port' abstraction to the iOS client app.
* @param {function(u2f.WrappedIosPort_)} callback
* @private
*/
u2f.getIosPort_ = function(callback) {
setTimeout(function() {
callback(new u2f.WrappedIosPort_());
}, 0);
};
/**
* A wrapper for chrome.runtime.Port that is compatible with MessagePort.
* @param {Port} port
* @constructor
* @private
*/
u2f.WrappedChromeRuntimePort_ = function(port) {
this.port_ = port;
};
/**
* Format and return a sign request compliant with the JS API version supported by the extension.
* @param {Array<u2f.SignRequest>} signRequests
* @param {number} timeoutSeconds
* @param {number} reqId
* @return {Object}
*/
u2f.formatSignRequest_ =
function(appId, challenge, registeredKeys, timeoutSeconds, reqId) {
if (js_api_version === undefined || js_api_version < 1.1) {
// Adapt request to the 1.0 JS API
var signRequests = [];
for (var i = 0; i < registeredKeys.length; i++) {
signRequests[i] = {
version: registeredKeys[i].version,
challenge: challenge,
keyHandle: registeredKeys[i].keyHandle,
appId: appId
};
}
return {
type: u2f.MessageTypes.U2F_SIGN_REQUEST,
signRequests: signRequests,
timeoutSeconds: timeoutSeconds,
requestId: reqId
};
}
// JS 1.1 API
return {
type: u2f.MessageTypes.U2F_SIGN_REQUEST,
appId: appId,
challenge: challenge,
registeredKeys: registeredKeys,
timeoutSeconds: timeoutSeconds,
requestId: reqId
};
};
/**
* Format and return a register request compliant with the JS API version supported by the extension..
* @param {Array<u2f.SignRequest>} signRequests
* @param {Array<u2f.RegisterRequest>} signRequests
* @param {number} timeoutSeconds
* @param {number} reqId
* @return {Object}
*/
u2f.formatRegisterRequest_ =
function(appId, registeredKeys, registerRequests, timeoutSeconds, reqId) {
if (js_api_version === undefined || js_api_version < 1.1) {
// Adapt request to the 1.0 JS API
for (var i = 0; i < registerRequests.length; i++) {
registerRequests[i].appId = appId;
}
var signRequests = [];
for (var i = 0; i < registeredKeys.length; i++) {
signRequests[i] = {
version: registeredKeys[i].version,
challenge: registerRequests[0],
keyHandle: registeredKeys[i].keyHandle,
appId: appId
};
}
return {
type: u2f.MessageTypes.U2F_REGISTER_REQUEST,
signRequests: signRequests,
registerRequests: registerRequests,
timeoutSeconds: timeoutSeconds,
requestId: reqId
};
}
// JS 1.1 API
return {
type: u2f.MessageTypes.U2F_REGISTER_REQUEST,
appId: appId,
registerRequests: registerRequests,
registeredKeys: registeredKeys,
timeoutSeconds: timeoutSeconds,
requestId: reqId
};
};
/**
* Posts a message on the underlying channel.
* @param {Object} message
*/
u2f.WrappedChromeRuntimePort_.prototype.postMessage = function(message) {
this.port_.postMessage(message);
};
/**
* Emulates the HTML 5 addEventListener interface. Works only for the
* onmessage event, which is hooked up to the chrome.runtime.Port.onMessage.
* @param {string} eventName
* @param {function({data: Object})} handler
*/
u2f.WrappedChromeRuntimePort_.prototype.addEventListener =
function(eventName, handler) {
var name = eventName.toLowerCase();
if (name == 'message' || name == 'onmessage') {
this.port_.onMessage.addListener(function(message) {
// Emulate a minimal MessageEvent object
handler({'data': message});
});
} else {
console.error('WrappedChromeRuntimePort only supports onMessage');
}
};
/**
* Wrap the Authenticator app with a MessagePort interface.
* @constructor
* @private
*/
u2f.WrappedAuthenticatorPort_ = function() {
this.requestId_ = -1;
this.requestObject_ = null;
}
/**
* Launch the Authenticator intent.
* @param {Object} message
*/
u2f.WrappedAuthenticatorPort_.prototype.postMessage = function(message) {
var intentUrl =
u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ +
';S.request=' + encodeURIComponent(JSON.stringify(message)) +
';end';
document.location = intentUrl;
};
/**
* Tells what type of port this is.
* @return {String} port type
*/
u2f.WrappedAuthenticatorPort_.prototype.getPortType = function() {
return "WrappedAuthenticatorPort_";
};
/**
* Emulates the HTML 5 addEventListener interface.
* @param {string} eventName
* @param {function({data: Object})} handler
*/
u2f.WrappedAuthenticatorPort_.prototype.addEventListener = function(eventName, handler) {
var name = eventName.toLowerCase();
if (name == 'message') {
var self = this;
/* Register a callback to that executes when
* chrome injects the response. */
window.addEventListener(
'message', self.onRequestUpdate_.bind(self, handler), false);
} else {
console.error('WrappedAuthenticatorPort only supports message');
}
};
/**
* Callback invoked when a response is received from the Authenticator.
* @param function({data: Object}) callback
* @param {Object} message message Object
*/
u2f.WrappedAuthenticatorPort_.prototype.onRequestUpdate_ =
function(callback, message) {
var messageObject = JSON.parse(message.data);
var intentUrl = messageObject['intentURL'];
var errorCode = messageObject['errorCode'];
var responseObject = null;
if (messageObject.hasOwnProperty('data')) {
responseObject = /** @type {Object} */ (
JSON.parse(messageObject['data']));
}
callback({'data': responseObject});
};
/**
* Base URL for intents to Authenticator.
* @const
* @private
*/
u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ =
'intent:#Intent;action=com.google.android.apps.authenticator.AUTHENTICATE';
/**
* Wrap the iOS client app with a MessagePort interface.
* @constructor
* @private
*/
u2f.WrappedIosPort_ = function() {};
/**
* Launch the iOS client app request
* @param {Object} message
*/
u2f.WrappedIosPort_.prototype.postMessage = function(message) {
var str = JSON.stringify(message);
var url = "u2f://auth?" + encodeURI(str);
location.replace(url);
};
/**
* Tells what type of port this is.
* @return {String} port type
*/
u2f.WrappedIosPort_.prototype.getPortType = function() {
return "WrappedIosPort_";
};
/**
* Emulates the HTML 5 addEventListener interface.
* @param {string} eventName
* @param {function({data: Object})} handler
*/
u2f.WrappedIosPort_.prototype.addEventListener = function(eventName, handler) {
var name = eventName.toLowerCase();
if (name !== 'message') {
console.error('WrappedIosPort only supports message');
}
};
/**
* Sets up an embedded trampoline iframe, sourced from the extension.
* @param {function(MessagePort)} callback
* @private
*/
u2f.getIframePort_ = function(callback) {
// Create the iframe
var iframeOrigin = 'chrome-extension://' + u2f.EXTENSION_ID;
var iframe = document.createElement('iframe');
iframe.src = iframeOrigin + '/u2f-comms.html';
iframe.setAttribute('style', 'display:none');
document.body.appendChild(iframe);
var channel = new MessageChannel();
var ready = function(message) {
if (message.data == 'ready') {
channel.port1.removeEventListener('message', ready);
callback(channel.port1);
} else {
console.error('First event on iframe port was not "ready"');
}
};
channel.port1.addEventListener('message', ready);
channel.port1.start();
iframe.addEventListener('load', function() {
// Deliver the port to the iframe and initialize
iframe.contentWindow.postMessage('init', iframeOrigin, [channel.port2]);
});
};
//High-level JS API
/**
* Default extension response timeout in seconds.
* @const
*/
u2f.EXTENSION_TIMEOUT_SEC = 30;
/**
* A singleton instance for a MessagePort to the extension.
* @type {MessagePort|u2f.WrappedChromeRuntimePort_}
* @private
*/
u2f.port_ = null;
/**
* Callbacks waiting for a port
* @type {Array<function((MessagePort|u2f.WrappedChromeRuntimePort_))>}
* @private
*/
u2f.waitingForPort_ = [];
/**
* A counter for requestIds.
* @type {number}
* @private
*/
u2f.reqCounter_ = 0;
/**
* A map from requestIds to client callbacks
* @type {Object.<number,(function((u2f.Error|u2f.RegisterResponse))
* |function((u2f.Error|u2f.SignResponse)))>}
* @private
*/
u2f.callbackMap_ = {};
/**
* Creates or retrieves the MessagePort singleton to use.
* @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback
* @private
*/
u2f.getPortSingleton_ = function(callback) {
if (u2f.port_) {
callback(u2f.port_);
} else {
if (u2f.waitingForPort_.length == 0) {
u2f.getMessagePort(function(port) {
u2f.port_ = port;
u2f.port_.addEventListener('message',
/** @type {function(Event)} */ (u2f.responseHandler_));
// Careful, here be async callbacks. Maybe.
while (u2f.waitingForPort_.length)
u2f.waitingForPort_.shift()(u2f.port_);
});
}
u2f.waitingForPort_.push(callback);
}
};
/**
* Handles response messages from the extension.
* @param {MessageEvent.<u2f.Response>} message
* @private
*/
u2f.responseHandler_ = function(message) {
var response = message.data;
var reqId = response['requestId'];
if (!reqId || !u2f.callbackMap_[reqId]) {
console.error('Unknown or missing requestId in response.');
return;
}
var cb = u2f.callbackMap_[reqId];
delete u2f.callbackMap_[reqId];
cb(response['responseData']);
};
/**
* Dispatches an array of sign requests to available U2F tokens.
* If the JS API version supported by the extension is unknown, it first sends a
* message to the extension to find out the supported API version and then it sends
* the sign request.
* @param {string=} appId
* @param {string=} challenge
* @param {Array<u2f.RegisteredKey>} registeredKeys
* @param {function((u2f.Error|u2f.SignResponse))} callback
* @param {number=} opt_timeoutSeconds
*/
u2f.sign = function(appId, challenge, registeredKeys, callback, opt_timeoutSeconds) {
if (js_api_version === undefined) {
// Send a message to get the extension to JS API version, then send the actual sign request.
u2f.getApiVersion(
function (response) {
js_api_version = response['js_api_version'] === undefined ? 0 : response['js_api_version'];
console.log("Extension JS API Version: ", js_api_version);
u2f.sendSignRequest(appId, challenge, registeredKeys, callback, opt_timeoutSeconds);
});
} else {
// We know the JS API version. Send the actual sign request in the supported API version.
u2f.sendSignRequest(appId, challenge, registeredKeys, callback, opt_timeoutSeconds);
}
};
/**
* Dispatches an array of sign requests to available U2F tokens.
* @param {string=} appId
* @param {string=} challenge
* @param {Array<u2f.RegisteredKey>} registeredKeys
* @param {function((u2f.Error|u2f.SignResponse))} callback
* @param {number=} opt_timeoutSeconds
*/
u2f.sendSignRequest = function(appId, challenge, registeredKeys, callback, opt_timeoutSeconds) {
u2f.getPortSingleton_(function(port) {
var reqId = ++u2f.reqCounter_;
u2f.callbackMap_[reqId] = callback;
var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ?
opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC);
var req = u2f.formatSignRequest_(appId, challenge, registeredKeys, timeoutSeconds, reqId);
port.postMessage(req);
});
};
/**
* Dispatches register requests to available U2F tokens. An array of sign
* requests identifies already registered tokens.
* If the JS API version supported by the extension is unknown, it first sends a
* message to the extension to find out the supported API version and then it sends
* the register request.
* @param {string=} appId
* @param {Array<u2f.RegisterRequest>} registerRequests
* @param {Array<u2f.RegisteredKey>} registeredKeys
* @param {function((u2f.Error|u2f.RegisterResponse))} callback
* @param {number=} opt_timeoutSeconds
*/
u2f.register = function(appId, registerRequests, registeredKeys, callback, opt_timeoutSeconds) {
if (js_api_version === undefined) {
// Send a message to get the extension to JS API version, then send the actual register request.
u2f.getApiVersion(
function (response) {
js_api_version = response['js_api_version'] === undefined ? 0: response['js_api_version'];
console.log("Extension JS API Version: ", js_api_version);
u2f.sendRegisterRequest(appId, registerRequests, registeredKeys,
callback, opt_timeoutSeconds);
});
} else {
// We know the JS API version. Send the actual register request in the supported API version.
u2f.sendRegisterRequest(appId, registerRequests, registeredKeys,
callback, opt_timeoutSeconds);
}
};
/**
* Dispatches register requests to available U2F tokens. An array of sign
* requests identifies already registered tokens.
* @param {string=} appId
* @param {Array<u2f.RegisterRequest>} registerRequests
* @param {Array<u2f.RegisteredKey>} registeredKeys
* @param {function((u2f.Error|u2f.RegisterResponse))} callback
* @param {number=} opt_timeoutSeconds
*/
u2f.sendRegisterRequest = function(appId, registerRequests, registeredKeys, callback, opt_timeoutSeconds) {
u2f.getPortSingleton_(function(port) {
var reqId = ++u2f.reqCounter_;
u2f.callbackMap_[reqId] = callback;
var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ?
opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC);
var req = u2f.formatRegisterRequest_(
appId, registeredKeys, registerRequests, timeoutSeconds, reqId);
port.postMessage(req);
});
};
/**
* Dispatches a message to the extension to find out the supported
* JS API version.
* If the user is on a mobile phone and is thus using Google Authenticator instead
* of the Chrome extension, don't send the request and simply return 0.
* @param {function((u2f.Error|u2f.GetJsApiVersionResponse))} callback
* @param {number=} opt_timeoutSeconds
*/
u2f.getApiVersion = function(callback, opt_timeoutSeconds) {
u2f.getPortSingleton_(function(port) {
// If we are using Android Google Authenticator or iOS client app,
// do not fire an intent to ask which JS API version to use.
if (port.getPortType) {
var apiVersion;
switch (port.getPortType()) {
case 'WrappedIosPort_':
case 'WrappedAuthenticatorPort_':
apiVersion = 1.1;
break;
default:
apiVersion = 0;
break;
}
callback({ 'js_api_version': apiVersion });
return;
}
var reqId = ++u2f.reqCounter_;
u2f.callbackMap_[reqId] = callback;
var req = {
type: u2f.MessageTypes.U2F_GET_API_VERSION_REQUEST,
timeoutSeconds: (typeof opt_timeoutSeconds !== 'undefined' ?
opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC),
requestId: reqId
};
port.postMessage(req);
});
};

View File

@ -4,18 +4,31 @@
<link rel="stylesheet" type="text/css" href="login.css">
</head>
<body>
<div class="login">
<h1>Login Portal</h1>
<div id="login-form">
<input type="text" name="username" id="username" placeholder="Username" required="required" />
<input type="password" name="password" id="password" placeholder="Password" required="required" />
<input type="text" name="token" id="token" placeholder="Verification token" required="required" />
<button type="button" id="login-button" class="btn btn-primary btn-block btn-large">Enter</button>
<div id="first-factor" class="login">
<h1>Login</h1>
<div id="login-form">
<input type="text" name="username" id="username" placeholder="Username" required="required" />
<input type="password" name="password" id="password" placeholder="Password" required="required" />
<button type="button" id="login-button" class="btn btn-primary btn-block btn-large">Enter</button>
</div>
</div>
<div id="second-factor" class="login" style="display: none;">
<h1>Second factor</h1>
<div id="totp">
<h2>Time-Based One-Time Password</h2>
<input type="text" name="totp-token" id="totp-token" placeholder="Validation token" />
<button type="button" id="totp-sign-button" class="btn btn-primary btn-block btn-large">Sign</button>
</div>
<div id="u2f">
<h2>FIDO Universal 2nd Factor</h2>
<button type="button" id="u2f-sign-button" class="btn btn-primary btn-block btn-large">Sign</button>
<button type="button" id="u2f-register-button" class="btn btn-primary btn-block btn-large">Register</button>
</div>
</div>
<div id="information" style="display: none;"></div>
</div>
</body>
<script src="jquery.min.js"></script>
<script src="js.cookie.min.js"></script>
<script src="notify.min.js"></script>
<script src="u2f-api.js"></script>
<script src="login.js"></script>
</html>

View File

@ -3,22 +3,16 @@ var request_ = require('request');
var assert = require('assert');
var speakeasy = require('speakeasy');
var j = request_.jar();
var request = request_.defaults({jar: j});
var Q = require('q');
var Promise = require('bluebird');
var request = Promise.promisifyAll(request_.defaults({jar: j}));
var BASE_URL = 'http://localhost:8080';
var BASE_URL = 'https://localhost:8080';
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
describe('test the server', function() {
var home_page;
var login_page;
var config = {
port: 8090,
totp_secret: 'totp_secret',
ldap_url: 'ldap://127.0.0.1:389',
ldap_users_dn: 'ou=users,dc=example,dc=com',
jwt_secret: 'jwt_secret',
jwt_expiration_time: '1h'
};
before(function() {
var home_page_promise = getHomePage()
@ -29,14 +23,14 @@ describe('test the server', function() {
.then(function(data) {
login_page = data.body;
});
return Q.all([home_page_promise,
login_page_promise]);
return Promise.all([home_page_promise,
login_page_promise]);
});
it('should serve the login page', function(done) {
getPromised(BASE_URL + '/auth/login?redirect=/')
.then(function(data) {
assert.equal(data.response.statusCode, 200);
assert.equal(data.statusCode, 200);
done();
});
});
@ -44,7 +38,7 @@ describe('test the server', function() {
it('should serve the homepage', function(done) {
getPromised(BASE_URL + '/')
.then(function(data) {
assert.equal(data.response.statusCode, 200);
assert.equal(data.statusCode, 200);
done();
});
});
@ -52,69 +46,71 @@ describe('test the server', function() {
it('should redirect when logout', function(done) {
getPromised(BASE_URL + '/auth/logout?redirect=/')
.then(function(data) {
assert.equal(data.response.statusCode, 200);
assert.equal(data.statusCode, 200);
assert.equal(data.body, home_page);
done();
});
});
it('should be redirected to the login page when accessing secret while not authenticated', function(done) {
getPromised(BASE_URL + '/secret.html')
it('should be redirected to the login page when accessing secret while not authenticated', function() {
return getPromised(BASE_URL + '/secret.html')
.then(function(data) {
assert.equal(data.response.statusCode, 200);
assert.equal(data.statusCode, 200);
assert.equal(data.body, login_page);
done();
return Promise.resolve();
});
});
it('should fail the login', function(done) {
postPromised(BASE_URL + '/_auth', {
it('should fail the first_factor login', function() {
return postPromised(BASE_URL + '/auth/_auth/1stfactor', {
form: {
username: 'admin',
password: 'password',
token: 'abc'
password: 'bad_password'
}
})
.then(function(data) {
assert.equal(data.body, 'Authentication failed');
done();
assert.equal(401, data.statusCode);
return Promise.resolve();
});
});
it('should login and access the secret', function(done) {
it('should login and access the secret using totp', function() {
var token = speakeasy.totp({
secret: 'GRWGIJS6IRHVEODVNRCXCOBMJ5AGC6ZE',
encoding: 'base32'
});
postPromised(BASE_URL + '/_auth', {
return postPromised(BASE_URL + '/auth/_auth/1stfactor', {
form: {
username: 'admin',
password: 'password',
token: token
}
})
.then(function(data) {
assert.equal(data.response.statusCode, 200);
assert.equal(data.body.length, 148);
var cookie = request.cookie('access_token=' + data.body);
j.setCookie(cookie, BASE_URL + '/_auth');
.then(function(response) {
assert.equal(response.statusCode, 204);
return postPromised(BASE_URL + '/auth/_auth/2ndfactor/totp', {
form: { token: token }
});
})
.then(function(response) {
assert.equal(response.statusCode, 204);
return getPromised(BASE_URL + '/secret.html');
})
.then(function(data) {
var content = data.body;
.then(function(response) {
var content = response.body;
var is_secret_page_content =
(content.indexOf('This is a very important secret!') > -1);
assert(is_secret_page_content);
done();
return Promise.resolve();
})
.fail(function(err) {
.catch(function(err) {
console.error(err);
return Promise.reject(err);
});
});
it('should logoff and should not be able to access secret anymore', function(done) {
getPromised(BASE_URL + '/secret.html')
it('should logoff and should not be able to access secret anymore', function() {
return getPromised(BASE_URL + '/secret.html')
.then(function(data) {
var content = data.body;
var is_secret_page_content =
@ -123,17 +119,18 @@ describe('test the server', function() {
return getPromised(BASE_URL + '/auth/logout')
})
.then(function(data) {
assert.equal(data.response.statusCode, 200);
assert.equal(data.statusCode, 200);
assert.equal(data.body, home_page);
return getPromised(BASE_URL + '/secret.html');
})
.then(function(data) {
var content = data.body;
assert.equal(data.body, login_page);
done();
return Promise.resolve();
})
.fail(function(err) {
.catch(function(err) {
console.error(err);
return Promise.reject();
});
});
});
@ -153,15 +150,13 @@ function responsePromised(defer) {
}
function getPromised(url) {
var defer = Q.defer();
request.get(url, responsePromised(defer));
return defer.promise;
console.log('GET: %s', url);
return request.getAsync(url);
}
function postPromised(url, body) {
var defer = Q.defer();
request.post(url, body, responsePromised(defer));
return defer.promise;
console.log('POST: %s, %s', url, JSON.stringify(body));
return request.postAsync(url, body);
}
function getHomePage() {

View File

@ -0,0 +1,83 @@
var sinon = require('sinon');
var Promise = require('bluebird');
var assert = require('assert');
var denyNotLogged = require('../../../src/lib/routes/deny_not_logged');
describe('test not logged', function() {
it('should return status code 401 when auth_session has not been previously created', function() {
return test_auth_session_not_created();
});
it('should return status code 401 when auth_session has failed first factor', function() {
return test_auth_first_factor_not_validated();
});
it('should return status code 204 when auth_session has succeeded first factor stage', function() {
return test_auth_with_first_factor_validated();
});
});
function test_auth_session_not_created() {
return new Promise(function(resolve, reject) {
var send = sinon.spy(resolve);
var status = sinon.spy(function(code) {
assert.equal(401, code);
});
var req = {
session: {}
}
var res = {
send: send,
status: status
}
denyNotLogged(reject)(req, res);
});
}
function test_auth_first_factor_not_validated() {
return new Promise(function(resolve, reject) {
var send = sinon.spy(resolve);
var status = sinon.spy(function(code) {
assert.equal(401, code);
});
var req = {
session: {
auth_session: {
first_factor: false,
second_factor: false
}
}
}
var res = {
send: send,
status: status
}
denyNotLogged(reject)(req, res);
});
}
function test_auth_with_first_factor_validated() {
return new Promise(function(resolve, reject) {
var req = {
session: {
auth_session: {
first_factor: true,
second_factor: false
}
}
}
var res = {
send: sinon.spy(),
status: sinon.spy()
}
denyNotLogged(resolve)(req, res);
});
}

View File

@ -5,54 +5,64 @@ var assert = require('assert');
var first_factor = require('../../../src/lib/routes/first_factor');
describe('test the first factor validation route', function() {
var req, res;
var ldap_interface_mock;
beforeEach(function() {
var bind_mock = sinon.stub();
ldap_interface_mock = {
bind: bind_mock
}
var config = {
ldap_users_dn: 'dc=example,dc=com'
}
var app_get = sinon.stub();
app_get.withArgs('ldap client').returns(ldap_interface_mock);
app_get.withArgs('config').returns(ldap_interface_mock);
req = {
app: {
get: app_get
},
body: {
username: 'username',
password: 'password'
},
session: {
auth_session: {
first_factor: false,
second_factor: false
}
}
}
res = {
send: sinon.spy(),
status: sinon.spy()
}
});
it('should return status code 204 when LDAP binding succeeds', function() {
return test_first_factor_promised({ error: undefined, data: undefined }, 204);
return new Promise(function(resolve, reject) {
res.send = sinon.spy(function(data) {
assert.equal('username', req.session.auth_session.userid);
assert.equal(204, res.status.getCall(0).args[0]);
resolve();
});
ldap_interface_mock.bind.yields(undefined);
first_factor(req, res);
});
});
it('should return status code 401 when LDAP binding fails', function() {
return test_first_factor_promised({ error: 'ldap failed', data: undefined }, 401);
return new Promise(function(resolve, reject) {
res.send = sinon.spy(function(data) {
assert.equal(401, res.status.getCall(0).args[0]);
resolve();
});
ldap_interface_mock.bind.yields('Bad credentials');
first_factor(req, res);
});
});
});
function test_first_factor_promised(bind_params, statusCode) {
return new Promise(function(resolve, reject) {
test_first_factor(bind_params, statusCode, resolve, reject);
});
}
function test_first_factor(bind_params, statusCode, resolve, reject) {
var send = sinon.spy(function(data) {
resolve();
});
var status = sinon.spy(function(code) {
assert.equal(code, statusCode);
});
var bind_mock = sinon.stub().yields(bind_params.error, bind_params.data);
var ldap_interface_mock = {
bind: bind_mock
}
var config = {
ldap_users_dn: 'dc=example,dc=com'
}
var app_get = sinon.stub();
app_get.withArgs('ldap client').returns(ldap_interface_mock);
app_get.withArgs('config').returns(ldap_interface_mock);
var req = {
app: {
get: app_get
},
body: {
username: 'username',
password: 'password'
}
}
var res = {
send: send,
status: status
}
first_factor(req, res);
}

View File

@ -0,0 +1,78 @@
var totp = require('../../../src/lib/routes/totp');
var Promise = require('bluebird');
var sinon = require('sinon');
var assert = require('assert');
describe('test totp route', function() {
var req, res;
var totp_engine;
beforeEach(function() {
var app_get = sinon.stub();
req = {
app: {
get: app_get
},
body: {
token: 'abc'
},
session: {
auth_session: {
first_factor: false,
second_factor: false
}
}
};
res = {
send: sinon.spy(),
status: sinon.spy()
};
var config = { totp_secret: 'secret' };
totp_engine = {
totp: sinon.stub()
}
app_get.withArgs('totp engine').returns(totp_engine);
app_get.withArgs('config').returns(config);
});
it('should send status code 204 when totp is valid', function() {
return new Promise(function(resolve, reject) {
totp_engine.totp.returns('abc');
res.send = sinon.spy(function() {
// Second factor passed
assert.equal(true, req.session.auth_session.second_factor)
assert.equal(204, res.status.getCall(0).args[0]);
resolve();
});
totp(req, res);
})
});
it('should send status code 401 when totp is not valid', function() {
return new Promise(function(resolve, reject) {
totp_engine.totp.returns('bad_token');
res.send = sinon.spy(function() {
assert.equal(false, req.session.auth_session.second_factor)
assert.equal(401, res.status.getCall(0).args[0]);
resolve();
});
totp(req, res);
})
});
it('should send status code 401 when session has not been initiated', function() {
return new Promise(function(resolve, reject) {
totp_engine.totp.returns('abc');
res.send = sinon.spy(function() {
assert.equal(401, res.status.getCall(0).args[0]);
resolve();
});
req.session = {};
totp(req, res);
})
});
});

View File

@ -0,0 +1,230 @@
var sinon = require('sinon');
var Promise = require('bluebird');
var assert = require('assert');
var u2f = require('../../../src/lib/routes/u2f');
var winston = require('winston');
describe('test u2f routes', function() {
var req, res;
beforeEach(function() {
req = {}
req.app = {};
req.app.get = sinon.stub();
req.app.get.withArgs('logger').returns(winston);
req.session = {};
req.session.auth_session = {};
req.session.auth_session.userid = 'user';
req.session.auth_session.first_factor = true;
req.session.auth_session.second_factor = false;
req.headers = {};
req.headers.host = 'localhost';
res = {};
res.send = sinon.spy();
res.status = sinon.spy();
})
describe('test registration request', test_registration_request);
describe('test registration', test_registration);
describe('test signing request', test_signing_request);
describe('test signing', test_signing);
function test_registration_request() {
it('should send back the registration request and save it in the session', function(done) {
var expectedRequest = {
test: 'abc'
};
res.json = sinon.spy(function(data) {
assert.equal(200, res.status.getCall(0).args[0]);
assert.deepEqual(expectedRequest, data);
done();
});
var user_key_container = {};
var u2f_mock = {};
u2f_mock.startRegistration = sinon.stub();
u2f_mock.startRegistration.returns(Promise.resolve(expectedRequest));
req.app.get.withArgs('u2f').returns(u2f_mock);
u2f(user_key_container).register_request(req, res);
});
it('should return internal error on registration request', function(done) {
res.send = sinon.spy(function(data) {
assert.equal(500, res.status.getCall(0).args[0]);
done();
});
var user_key_container = {};
var u2f_mock = {};
u2f_mock.startRegistration = sinon.stub();
u2f_mock.startRegistration.returns(Promise.reject('Internal error'));
req.app.get.withArgs('u2f').returns(u2f_mock);
u2f(user_key_container).register_request(req, res);
});
}
function test_registration() {
it('should return status code 200', function(done) {
var user_key_container = {};
var expectedStatus = {
keyHandle: 'keyHandle',
publicKey: 'pbk',
certificate: 'cert'
};
res.send = sinon.spy(function(data) {
assert('user' in user_key_container);
assert.deepEqual(expectedStatus, user_key_container['user']);
done();
});
var u2f_mock = {};
u2f_mock.finishRegistration = sinon.stub();
u2f_mock.finishRegistration.returns(Promise.resolve(expectedStatus));
req.session.auth_session.register_request = {};
req.app.get.withArgs('u2f').returns(u2f_mock);
u2f(user_key_container).register(req, res);
});
it('should return unauthorized error on registration request', function(done) {
res.send = sinon.spy(function(data) {
assert.equal(401, res.status.getCall(0).args[0]);
done();
});
var user_key_container = {};
var u2f_mock = {};
u2f_mock.finishRegistration = sinon.stub();
u2f_mock.finishRegistration.returns(Promise.reject('Internal error'));
req.app.get.withArgs('u2f').returns(u2f_mock);
u2f(user_key_container).register(req, res);
});
it('should return unauthorized error when no auth request has been initiated', function(done) {
res.send = sinon.spy(function(data) {
assert.equal(401, res.status.getCall(0).args[0]);
done();
});
var user_key_container = {};
var u2f_mock = {};
u2f_mock.finishRegistration = sinon.stub();
u2f_mock.finishRegistration.returns(Promise.resolve());
req.app.get.withArgs('u2f').returns(u2f_mock);
u2f(user_key_container).register(req, res);
});
}
function test_signing_request() {
it('should send back the sign request and save it in the session', function(done) {
var expectedRequest = {
test: 'abc'
};
res.json = sinon.spy(function(data) {
assert.deepEqual(expectedRequest, req.session.auth_session.sign_request);
assert.equal(200, res.status.getCall(0).args[0]);
assert.deepEqual(expectedRequest, data);
done();
});
var user_key_container = {};
user_key_container['user'] = {}; // simulate a registration
var u2f_mock = {};
u2f_mock.startAuthentication = sinon.stub();
u2f_mock.startAuthentication.returns(Promise.resolve(expectedRequest));
req.app.get.withArgs('u2f').returns(u2f_mock);
u2f(user_key_container).sign_request(req, res);
});
it('should return unauthorized error on registration request error', function(done) {
res.send = sinon.spy(function(data) {
assert.equal(401, res.status.getCall(0).args[0]);
done();
});
var user_key_container = {};
user_key_container['user'] = {}; // simulate a registration
var u2f_mock = {};
u2f_mock.startAuthentication = sinon.stub();
u2f_mock.startAuthentication.returns(Promise.reject('Internal error'));
req.app.get.withArgs('u2f').returns(u2f_mock);
u2f(user_key_container).sign_request(req, res);
});
it('should send unauthorized error when no registration exists', function(done) {
var expectedRequest = {
test: 'abc'
};
res.send = sinon.spy(function(data) {
assert.equal(401, res.status.getCall(0).args[0]);
done();
});
var user_key_container = {}; // no entry means no registration
var u2f_mock = {};
u2f_mock.startAuthentication = sinon.stub();
u2f_mock.startAuthentication.returns(Promise.resolve(expectedRequest));
req.app.get.withArgs('u2f').returns(u2f_mock);
u2f(user_key_container).sign_request(req, res);
});
}
function test_signing() {
it('should return status code 204', function(done) {
var user_key_container = {};
user_key_container['user'] = {};
var expectedStatus = {
keyHandle: 'keyHandle',
publicKey: 'pbk',
certificate: 'cert'
};
res.send = sinon.spy(function(data) {
assert(204, res.status.getCall(0).args[0]);
assert(req.session.auth_session.second_factor);
done();
});
var u2f_mock = {};
u2f_mock.finishAuthentication = sinon.stub();
u2f_mock.finishAuthentication.returns(Promise.resolve(expectedStatus));
req.session.auth_session.sign_request = {};
req.app.get.withArgs('u2f').returns(u2f_mock);
u2f(user_key_container).sign(req, res);
});
it('should return unauthorized error on registration request internal error', function(done) {
res.send = sinon.spy(function(data) {
assert.equal(401, res.status.getCall(0).args[0]);
done();
});
var user_key_container = {};
user_key_container['user'] = {};
var u2f_mock = {};
u2f_mock.finishAuthentication = sinon.stub();
u2f_mock.finishAuthentication.returns(Promise.reject('Internal error'));
req.session.auth_session.sign_request = {};
req.app.get.withArgs('u2f').returns(u2f_mock);
u2f(user_key_container).register(req, res);
});
it('should return unauthorized error when no sign request has been initiated', function(done) {
res.send = sinon.spy(function(data) {
assert.equal(401, res.status.getCall(0).args[0]);
done();
});
var user_key_container = {};
var u2f_mock = {};
u2f_mock.finishAuthentication = sinon.stub();
u2f_mock.finishAuthentication.returns(Promise.resolve());
req.app.get.withArgs('u2f').returns(u2f_mock);
u2f(user_key_container).register(req, res);
});
}
});

View File

@ -0,0 +1,63 @@
var assert = require('assert');
var verify = require('../../../src/lib/routes/verify');
var sinon = require('sinon');
describe('test authentication token verification', function() {
var req, res;
beforeEach(function() {
req = {};
res = {};
res.status = sinon.spy();
});
it('should be already authenticated', function(done) {
req.session = {};
req.session.auth_session = {first_factor: true, second_factor: true};
res.send = sinon.spy(function() {
assert.equal(204, res.status.getCall(0).args[0]);
done();
});
verify(req, res);
});
describe('given different cases of session', function() {
function test_unauthorized(auth_session) {
return new Promise(function(resolve, reject) {
req.session = {};
req.session.auth_session = auth_session;
res.send = sinon.spy(function() {
assert.equal(401, res.status.getCall(0).args[0]);
resolve();
});
verify(req, res);
});
}
it('should not be authenticated when second factor is missing', function() {
return test_unauthorized({ first_factor: true, second_factor: false });
});
it('should not be authenticated when first factor is missing', function() {
return test_unauthorized({ first_factor: false, second_factor: true });
});
it('should not be authenticated when first and second factor are missing', function() {
return test_unauthorized({ first_factor: false, second_factor: false });
});
it('should not be authenticated when session has not be initiated', function() {
return test_unauthorized(undefined);
});
it('should not be authenticated when session is partially initialized', function() {
return test_unauthorized({ first_factor: true });
});
});
});

View File

@ -1,105 +0,0 @@
var assert = require('assert');
var authentication = require('../../src/lib/authentication');
var create_res_mock = require('./res_mock');
var sinon = require('sinon');
var sinonPromise = require('sinon-promise');
sinonPromise(sinon);
var autoResolving = sinon.promise().resolves();
function create_req_mock(token) {
return {
body: {
username: 'username',
password: 'password',
token: token
},
cookies: {
'access_token': 'cookie_token'
},
app: {
get: sinon.stub()
}
}
}
function create_mocks() {
var totp_token = 'totp_token';
var jwt_token = 'jwt_token';
var res_mock = create_res_mock();
var req_mock = create_req_mock(totp_token);
var bind_mock = sinon.mock();
var totp_mock = sinon.mock();
var sign_mock = sinon.mock();
var verify_mock = sinon.promise();
var jwt = {
sign: sign_mock,
verify: verify_mock
};
var ldap_interface_mock = {
bind: bind_mock
};
var totp_interface_mock = {
totp: totp_mock
};
bind_mock.yields();
totp_mock.returns(totp_token);
sign_mock.returns(jwt_token);
var args = {
totp_secret: 'totp_secret',
jwt: jwt,
jwt_expiration_time: '1h',
users_dn: 'dc=example,dc=com',
ldap_interface: ldap_interface_mock,
totp_interface: totp_interface_mock
}
req_mock.app.get.withArgs('ldap client').returns(args.ldap_interface);
req_mock.app.get.withArgs('jwt engine').returns(args.jwt);
req_mock.app.get.withArgs('totp engine').returns(args.totp_interface);
req_mock.app.get.withArgs('config').returns({
totp_secret: 'totp_secret',
ldap_users_dn: 'ou=users,dc=example,dc=com'
});
return {
req: req_mock,
res: res_mock,
args: args,
totp: totp_mock,
jwt: jwt
}
}
describe('test authentication token verification', function() {
it('should be already authenticated', function(done) {
var mocks = create_mocks();
var data = { user: 'username' };
mocks.req.app.get.withArgs('jwt engine').returns({
verify: sinon.promise().resolves(data)
});
authentication.verify(mocks.req, mocks.res)
.then(function(actual_data) {
assert.equal(actual_data, data);
done();
});
});
it('should not be already authenticated', function(done) {
var mocks = create_mocks();
var data = { user: 'username' };
mocks.req.app.get.withArgs('jwt engine').returns({
verify: sinon.promise().rejects('Error with JWT token')
});
return authentication.verify(mocks.req, mocks.res, mocks.args)
.fail(function() {
done();
});
});
});

View File

@ -1,39 +0,0 @@
var Jwt = require('../../src/lib/jwt');
var sinon = require('sinon');
describe('test jwt', function() {
it('should sign and verify the token', function() {
var data = {user: 'user'};
var secret = 'secret';
var jwt = new Jwt(secret);
return jwt.sign(data, '1m')
.then(function(token) {
return jwt.verify(token);
});
});
it('should verify and fail on wrong token', function() {
var jwt = new Jwt('secret');
var token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoidXNlciIsImlhdCI6MTQ4NDc4NTExMywiZXhwIjoaNDg0Nzg1MTczfQ.yZOZEaMDyOn0tSDiDSPYl4ZP2oL3FQ-Vrzds7hYcNio';
return jwt.verify(token).catch(function() {
return Promise.resolve();
});
});
it('should fail after expiry', function() {
var clock = sinon.useFakeTimers(0);
var data = { user: 'user' };
var jwt = new Jwt('secret');
return jwt.sign(data, '1m')
.then(function(token) {
clock.tick(1000 * 61); // 61 seconds
return jwt.verify(token);
})
.catch(function() {
clock.restore();
return Promise.resolve();
});
});
});

View File

@ -0,0 +1,52 @@
var ldap = require('../../src/lib/ldap');
var sinon = require('sinon');
var Promise = require('bluebird');
var assert = require('assert');
describe('test ldap validation', function() {
var ldap_client;
beforeEach(function() {
ldap_client = {
bind: sinon.stub()
}
});
function test_validate() {
var username = 'user';
var password = 'password';
var ldap_url = 'http://ldap';
var users_dn = 'dc=example,dc=com';
return ldap.validate(ldap_client, username, password, ldap_url, users_dn);
}
it('should bind the user if good credentials provided', function() {
ldap_client.bind.yields();
return test_validate();
});
// cover an issue with promisify context
it('should promisify correctly', function() {
function LdapClient() {
this.test = 'abc';
}
LdapClient.prototype.bind = function(username, password, fn) {
assert.equal('abc', this.test);
fn();
}
ldap_client = new LdapClient();
return test_validate();
});
it('should not bind the user if wrong credentials provided', function() {
ldap_client.bind.yields('wrong credentials');
var promise = test_validate();
return promise.catch(function() {
return Promise.resolve();
});
});
});

View File

@ -1,35 +0,0 @@
var ldap = require('../../src/lib/ldap');
var sinon = require('sinon');
var sinonPromise = require('sinon-promise');
sinonPromise(sinon);
var autoResolving = sinon.promise().resolves();
function test_validate(bind_mock) {
var username = 'user';
var password = 'password';
var ldap_url = 'http://ldap';
var users_dn = 'dc=example,dc=com';
var ldap_client_mock = {
bind: bind_mock
}
return ldap.validate(ldap_client_mock, username, password, ldap_url, users_dn);
}
describe('test ldap validation', function() {
it('should bind the user if good credentials provided', function() {
var bind_mock = sinon.mock().yields();
return test_validate(bind_mock);
});
it('should not bind the user if wrong credentials provided', function() {
var bind_mock = sinon.mock().yields('wrong credentials');
var promise = test_validate(bind_mock);
return promise.error(autoResolving);
});
});

View File

@ -1,52 +0,0 @@
var replies = require('../../src/lib/replies');
var assert = require('assert');
var sinon = require('sinon');
var sinonPromise = require('sinon-promise');
sinonPromise(sinon);
var autoResolving = sinon.promise().resolves();
function create_res_mock() {
var status_mock = sinon.mock();
var send_mock = sinon.mock();
var set_mock = sinon.mock();
return {
status: status_mock,
send: send_mock,
set: set_mock
};
}
describe('test jwt', function() {
it('should authenticate with success', function() {
var res_mock = create_res_mock();
var username = 'username';
replies.authentication_succeeded(res_mock, username);
assert(res_mock.status.calledWith(200));
assert(res_mock.set.calledWith({'X-Remote-User': username }));
});
it('should reply successfully when already authenticated', function() {
var res_mock = create_res_mock();
var username = 'username';
replies.already_authenticated(res_mock, username);
assert(res_mock.status.calledWith(204));
assert(res_mock.set.calledWith({'X-Remote-User': username }));
});
it('should reply with failed authentication', function() {
var res_mock = create_res_mock();
var username = 'username';
replies.authentication_failed(res_mock, username);
assert(res_mock.status.calledWith(401));
});
});

View File

@ -1,6 +1,5 @@
var server = require('../../src/lib/server');
var Jwt = require('../../src/lib/jwt');
var request = require('request');
var assert = require('assert');
@ -13,29 +12,40 @@ var request = Promise.promisifyAll(request);
var BASE_URL = 'http://localhost:8090';
describe('test the server', function() {
var jwt = new Jwt('jwt_secret');
var _server
var u2f;
var ldap_client = {
bind: sinon.stub()
};
before(function() {
beforeEach(function(done) {
var config = {
port: 8090,
totp_secret: 'totp_secret',
ldap_url: 'ldap://127.0.0.1:389',
ldap_users_dn: 'ou=users,dc=example,dc=com',
jwt_secret: 'jwt_secret',
jwt_expiration_time: '1h'
session_secret: 'session_secret',
session_max_age: 50000
};
// ldap_client.bind.yields(undefined);
u2f = {};
u2f.startRegistration = sinon.stub();
u2f.finishRegistration = sinon.stub();
u2f.startAuthentication = sinon.stub();
u2f.finishAuthentication = sinon.stub();
ldap_client.bind.withArgs('cn=test_ok,ou=users,dc=example,dc=com',
'password').yields(undefined);
ldap_client.bind.withArgs('cn=test_nok,ou=users,dc=example,dc=com',
'password').yields('error');
server.run(config, ldap_client);
_server = server.run(config, ldap_client, u2f, function() {
done();
});
});
afterEach(function() {
_server.close();
  });
describe('test GET /login', function() {
test_login()
@ -45,119 +55,144 @@ describe('test the server', function() {
test_logout()
});
describe('test GET /_auth', function() {
test_get_auth(jwt);
});
describe('test POST /_auth/1stfactor', function() {
test_post_auth_1st_factor();
});
});
function test_login() {
it('should serve the login page', function(done) {
request.get(BASE_URL + '/login')
.on('response', function(response) {
assert.equal(response.statusCode, 200);
done();
})
});
}
function test_logout() {
it('should logout and redirect to /', function(done) {
request.get(BASE_URL + '/logout')
.on('response', function(response) {
assert.equal(response.req.path, '/');
done();
})
});
}
function test_get_auth(jwt) {
it('should return status code 401 when user is not authenticated', function(done) {
request.get(BASE_URL + '/_auth')
.on('response', function(response) {
assert.equal(response.statusCode, 401);
done();
})
describe('test authentication and verification', function() {
test_authentication();
});
it('should return status code 204 when user is authenticated', function(done) {
var j = request.jar();
var r = request.defaults({jar: j});
jwt.sign({ user: 'test' }, '1h')
.then(function(token) {
var cookie = r.cookie('access_token=' + token);
j.setCookie(cookie, BASE_URL + '/_auth');
r.get(BASE_URL + '/_auth')
.on('response', function(response) {
assert.equal(response.statusCode, 204);
function test_login() {
it('should serve the login page', function(done) {
request.getAsync(BASE_URL + '/login')
.then(function(response) {
assert.equal(response.statusCode, 200);
done();
});
});
});
}
function test_post_auth_1st_factor() {
it('should return status code 204 when ldap bind is successful', function() {
request.postAsync(BASE_URL + '/_auth/1stfactor', {
form: {
username: 'username',
password: 'password'
}
})
.then(function(response) {
assert.equal(response.statusCode, 204);
return Promise.resolve();
}
function test_logout() {
it('should logout and redirect to /', function(done) {
request.getAsync(BASE_URL + '/logout')
.then(function(response) {
assert.equal(response.req.path, '/');
done();
});
});
});
}
// function test_post_auth_totp() {
// it('should return the JWT token when authentication is successful', function(done) {
// var clock = sinon.useFakeTimers();
// var real_token = speakeasy.totp({
// secret: 'totp_secret',
// encoding: 'base32'
// });
// var expectedJwt = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoidGVzdF9vayIsImlhdCI6MCwiZXhwIjozNjAwfQ.ihvaljGjO5h3iSO_h3PkNNSCYeePyB8Hr5lfVZZYyrQ';
//
// request.post(BASE_URL + '/_auth/totp', {
// form: {
// username: 'test_ok',
// password: 'password',
// token: real_token
// }
// },
// function (error, response, body) {
// if (!error && response.statusCode == 200) {
// assert.equal(body, expectedJwt);
// clock.restore();
// done();
// }
// });
// });
//
// it('should return invalid authentication status code', function(done) {
// var clock = sinon.useFakeTimers();
// var real_token = speakeasy.totp({
// secret: 'totp_secret',
// encoding: 'base32'
// });
// var data = {
// form: {
// username: 'test_nok',
// password: 'password',
// token: real_token
// }
// }
//
// request.post(BASE_URL + '/_auth/totp', data, function (error, response, body) {
// if(response.statusCode == 401) {
// clock.restore();
// done();
// }
// });
// });
// }
}
function test_authentication() {
it('should return status code 401 when user is not authenticated', function() {
return request.getAsync({ url: BASE_URL + '/_verify' })
.then(function(response) {
assert.equal(response.statusCode, 401);
return Promise.resolve();
});
});
it('should return status code 204 when user is authenticated using totp', function() {
var real_token = speakeasy.totp({
secret: 'totp_secret',
encoding: 'base32'
});
var j = request.jar();
return request.getAsync({ url: BASE_URL + '/login', jar: j })
.then(function(res) {
assert.equal(res.statusCode, 200, 'get login page failed');
return request.postAsync({
url: BASE_URL + '/_auth/1stfactor',
jar: j,
form: {
username: 'test_ok',
password: 'password'
}
});
})
.then(function(res) {
assert.equal(res.statusCode, 204, 'first factor failed');
return request.postAsync({
url: BASE_URL + '/_auth/2ndfactor/totp',
jar: j,
form: {
token: real_token
}
});
})
.then(function(res) {
assert.equal(res.statusCode, 204, 'second factor failed');
return request.getAsync({ url: BASE_URL + '/_verify', jar: j })
})
.then(function(res) {
assert.equal(res.statusCode, 204, 'verify failed');
return Promise.resolve();
});
});
it('should return status code 204 when user is authenticated using u2f', function() {
var sign_request = {};
var sign_status = {};
var registration_request = {};
var registration_status = {};
u2f.startRegistration.returns(Promise.resolve(sign_request));
u2f.finishRegistration.returns(Promise.resolve(sign_status));
u2f.startAuthentication.returns(Promise.resolve(registration_request));
u2f.finishAuthentication.returns(Promise.resolve(registration_status));
var j = request.jar();
return request.getAsync({ url: BASE_URL + '/login', jar: j })
.then(function(res) {
assert.equal(res.statusCode, 200, 'get login page failed');
return request.postAsync({
url: BASE_URL + '/_auth/1stfactor',
jar: j,
form: {
username: 'test_ok',
password: 'password'
}
});
})
.then(function(res) {
assert.equal(res.statusCode, 204, 'first factor failed');
return request.getAsync({
url: BASE_URL + '/_auth/2ndfactor/u2f/register_request',
jar: j
});
})
.then(function(res) {
assert.equal(res.statusCode, 200, 'second factor, start register failed');
return request.postAsync({
url: BASE_URL + '/_auth/2ndfactor/u2f/register',
jar: j,
form: {
s: 'test'
}
});
})
.then(function(res) {
assert.equal(res.statusCode, 204, 'second factor, finish register failed');
return request.getAsync({
url: BASE_URL + '/_auth/2ndfactor/u2f/sign_request',
jar: j
});
})
.then(function(res) {
assert.equal(res.statusCode, 200, 'second factor, start sign failed');
return request.postAsync({
url: BASE_URL + '/_auth/2ndfactor/u2f/sign',
jar: j,
form: {
s: 'test'
}
});
})
.then(function(res) {
assert.equal(res.statusCode, 204, 'second factor, finish sign failed');
return request.getAsync({ url: BASE_URL + '/_verify', jar: j })
})
.then(function(res) {
assert.equal(res.statusCode, 204, 'verify failed');
return Promise.resolve();
});
});
}
});

View File

@ -1,12 +1,9 @@
var totp = require('../../src/lib/totp');
var sinon = require('sinon');
var sinonPromise = require('sinon-promise');
sinonPromise(sinon);
var Promise = require('bluebird');
var autoResolving = sinon.promise().resolves();
describe('test TOTP checker', function() {
describe('test TOTP validation', function() {
it('should validate the TOTP token', function() {
var totp_secret = 'NBD2ZV64R9UV1O7K';
var token = 'token';
@ -26,7 +23,10 @@ describe('test TOTP checker', function() {
var speakeasy_mock = {
totp: totp_mock
}
return totp.validate(speakeasy_mock, token, totp_secret).fail(autoResolving);
return totp.validate(speakeasy_mock, token, totp_secret)
.catch(function() {
return Promise.resolve();
});
});
});