Implement FIDO u2f authentication
parent
8c743228bf
commit
9670b23a8b
|
@ -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
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
|
||||
version: '2'
|
||||
services:
|
||||
auth:
|
||||
volumes:
|
||||
- ./src/views:/usr/src/views
|
||||
- ./src/public_html:/usr/src/public_html
|
||||
|
|
@ -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"
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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-----
|
|
@ -0,0 +1,11 @@
|
|||
-----BEGIN CERTIFICATE REQUEST-----
|
||||
MIIBhDCB7gIBADBFMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEh
|
||||
MB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIGfMA0GCSqGSIb3DQEB
|
||||
AQUAA4GNADCBiQKBgQDNloThcTVDIU1uscEdGFIDndqcCwnmYF4bs6bpJ1BxkBhq
|
||||
GUNYRu12hjiHLSA80ZwhNxZ4T5YD4+81Gvs9zMoSGd4jJRSBX6evPTXR8zkmcQI/
|
||||
EtN7WgXOXhTx3JiIGlPOI3OGJR+rvfc9FiNx30FC1wpw3gt2ZC+NQeedDvdPKwID
|
||||
AQABoAAwDQYJKoZIhvcNAQELBQADgYEAmCX60kspIw1Zfb79AQOarFW5Q2K2h5Vx
|
||||
/cRbDyHlKtbmG77EtICccULyqf76B1gNRw5Zq3lSotSUcLzsWcdesXCFDC7k87Qf
|
||||
mpQKPj6GdTYJvdWf8aDwt32tAqWuBIRoAbdx5WbFPPWVfDcm7zDJefBrhNUDH0Qd
|
||||
vcnxjvPMmOM=
|
||||
-----END CERTIFICATE REQUEST-----
|
|
@ -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-----
|
12
package.json
12
package.json
|
@ -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",
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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(){})});
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
||||
})();
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -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);
|
||||
});
|
||||
};
|
|
@ -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>
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
})
|
||||
});
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -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 });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
@ -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));
|
||||
});
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
Loading…
Reference in New Issue