Refactor client to make it responsive and testable

This commit is contained in:
Clement Michaud 2017-05-25 15:09:29 +02:00
parent 976dd6b87c
commit ddf1e48535
219 changed files with 5708 additions and 5573 deletions

5
.gitignore vendored
View File

@ -2,6 +2,9 @@
# NodeJs modules
node_modules/
# npm debug logs
npm-debug.log*
# Coverage reports
coverage/
@ -24,3 +27,5 @@ notifications/
# Generated by TypeScript compiler
dist/
.nyc_output/

View File

@ -20,7 +20,7 @@ addons:
before_install: npm install -g npm@'>=2.13.5'
script:
- grunt test
- grunt build
- grunt dist
- grunt docker-build
- docker-compose build
- docker-compose up -d

View File

@ -5,7 +5,7 @@ WORKDIR /usr/src
COPY package.json /usr/src/package.json
RUN npm install --production
COPY dist/src /usr/src
COPY dist/src/server /usr/src
ENV PORT=80
EXPOSE 80

View File

@ -1,10 +1,12 @@
module.exports = function(grunt) {
module.exports = function (grunt) {
const buildDir = "dist";
grunt.initConfig({
run: {
options: {},
"build-ts": {
"build": {
cmd: "npm",
args: ['run', 'build-ts']
args: ['run', 'build']
},
"tslint": {
cmd: "npm",
@ -17,39 +19,136 @@ module.exports = function(grunt) {
"docker-build": {
cmd: "docker",
args: ['build', '-t', 'clems4ever/authelia', '.']
},
"docker-restart": {
cmd: "docker-compose",
args: ['-f', 'docker-compose.yml', '-f', 'docker-compose.dev.yml', 'restart', 'auth']
},
"minify": {
cmd: "./node_modules/.bin/uglifyjs",
args: [`${buildDir}/src/server/public_html/js/authelia.js`, '-o', `${buildDir}/src/server/public_html/js/authelia.min.js`]
},
"apidoc": {
cmd: "./node_modules/.bin/apidoc",
args: ["-i", "src/server", "-o", "doc"]
}
},
copy: {
resources: {
expand: true,
cwd: 'src/resources/',
cwd: 'src/server/resources/',
src: '**',
dest: 'dist/src/resources/'
dest: `${buildDir}/src/server/resources/`
},
views: {
expand: true,
cwd: 'src/views/',
cwd: 'src/server/views/',
src: '**',
dest: 'dist/src/views/'
dest: `${buildDir}/src/server/views/`
},
public_html: {
images: {
expand: true,
cwd: 'src/public_html/',
cwd: 'src/client/img',
src: '**',
dest: 'dist/src/public_html/'
dest: `${buildDir}/src/server/public_html/img/`
},
thirdparties: {
expand: true,
cwd: 'src/client/thirdparties',
src: '**',
dest: `${buildDir}/src/server/public_html/js/`
},
},
browserify: {
dist: {
src: ['dist/src/client/index.js'],
dest: `${buildDir}/src/server/public_html/js/authelia.js`,
options: {
browserifyOptions: {
standalone: 'authelia'
},
},
},
},
watch: {
views: {
files: ['src/server/views/**/*.pug'],
tasks: ['copy:views'],
options: {
interrupt: false,
atBegin: true
}
},
resources: {
files: ['src/server/resources/*.ejs'],
tasks: ['copy:resources'],
options: {
interrupt: false,
atBegin: true
}
},
images: {
files: ['src/client/img/**'],
tasks: ['copy:images'],
options: {
interrupt: false,
atBegin: true
}
},
css: {
files: ['src/client/**/*.css'],
tasks: ['concat:css', 'cssmin'],
options: {
interrupt: true,
atBegin: true
}
},
client: {
files: ['src/client/**/*.ts', 'test/client/**/*.ts'],
tasks: ['build'],
options: {
interrupt: true,
atBegin: true
}
},
server: {
files: ['src/server/**/*.ts', 'test/server/**/*.ts'],
tasks: ['build', 'run:docker-restart'],
options: {
interrupt: true,
}
}
},
concat: {
css: {
src: ['src/client/css/*.css'],
dest: `${buildDir}/src/server/public_html/css/authelia.css`
},
},
cssmin: {
target: {
files: {
[`${buildDir}/src/server/public_html/css/authelia.min.css`]: [`${buildDir}/src/server/public_html/css/authelia.css`]
}
}
}
});
grunt.loadNpmTasks('grunt-run');
grunt.loadNpmTasks('grunt-browserify');
grunt.loadNpmTasks('grunt-contrib-concat');
grunt.loadNpmTasks('grunt-contrib-copy');
grunt.loadNpmTasks('grunt-contrib-cssmin');
grunt.loadNpmTasks('grunt-contrib-watch');
grunt.loadNpmTasks('grunt-run');
grunt.registerTask('default', ['build']);
grunt.registerTask('res', ['copy:resources', 'copy:views', 'copy:public_html']);
grunt.registerTask('build-resources', ['copy:resources', 'copy:views', 'copy:images', 'copy:thirdparties', 'concat:css', 'cssmin']);
grunt.registerTask('build', ['run:tslint', 'run:build', 'browserify:dist']);
grunt.registerTask('dist', ['build', 'build-resources', 'run:minify', 'cssmin']);
grunt.registerTask('build', ['run:tslint', 'run:build-ts', 'res']);
grunt.registerTask('docker-build', ['run:docker-build']);
grunt.registerTask('docker-restart', ['run:docker-restart']);
grunt.registerTask('test', ['run:test']);
};

View File

@ -117,6 +117,8 @@ email address. For the sake of the example, the email is delivered in the file
./notifications/notification.txt.
Paste the link in your browser and you should be able to reset the password.
![reset-password](https://raw.githubusercontent.com/clems4ever/authelia/master/images/reset_password.png)
### Access Control
With **Authelia**, you can define your own access control rules for restricting
the access to certain subdomains to your users. Those rules are defined in the

View File

@ -76,7 +76,7 @@ session:
# The directory where the DB files will be saved
store_directory: /var/lib/auth-server/store
store_directory: /var/lib/authelia/store
# Notifications are sent to users when they require a password reset, a u2f

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,15 +1,15 @@
define({
"title": "Authelia API documentation",
"name": "authelia",
"version": "1.0.11",
"description": "2-factor authentication server using LDAP as 1st factor and TOTP or U2F as 2nd factor",
"version": "2.1.3",
"description": "2FA Single Sign-On server for nginx using LDAP, TOTP and U2F",
"sampleUrl": false,
"defaultVersion": "0.0.0",
"apidoc": "0.3.0",
"generator": {
"name": "apidoc",
"time": "2017-01-29T00:44:17.687Z",
"time": "2017-06-11T20:41:36.025Z",
"url": "http://apidocjs.com",
"version": "0.17.5"
"version": "0.17.6"
}
});

View File

@ -1,15 +1,15 @@
{
"title": "Authelia API documentation",
"name": "authelia",
"version": "1.0.11",
"description": "2-factor authentication server using LDAP as 1st factor and TOTP or U2F as 2nd factor",
"version": "2.1.3",
"description": "2FA Single Sign-On server for nginx using LDAP, TOTP and U2F",
"sampleUrl": false,
"defaultVersion": "0.0.0",
"apidoc": "0.3.0",
"generator": {
"name": "apidoc",
"time": "2017-01-29T00:44:17.687Z",
"time": "2017-06-11T20:41:36.025Z",
"url": "http://apidocjs.com",
"version": "0.17.5"
"version": "0.17.6"
}
}

View File

@ -172,6 +172,7 @@ pre {
border-radius: 6px;
position: relative;
margin: 10px 0 20px 0;
overflow-x: auto;
}
pre.prettyprint {

View File

@ -224,7 +224,7 @@
<div class="tab-content">
{{#each params.examples}}
<div class="tab-pane{{#if_eq @index compare=0}} active{{/if_eq}}" id="{{../section}}-examples-{{../id}}-{{@index}}">
<pre class="prettyprint language-{{type}}" data-type="{{type}}"><code>{{{reformat content type}}}</code></pre>
<pre class="prettyprint language-{{type}}" data-type="{{type}}"><code>{{reformat content type}}</code></pre>
</div>
{{/each}}
</div>
@ -274,7 +274,7 @@
{{#each this}}
<label class="col-md-3 control-label" for="sample-request-param-field-{{field}}">{{field}}</label>
<div class="input-group">
<input id="sample-request-param-field-{{field}}" type="text" placeholder="{{field}}" class="form-control sample-request-param" data-sample-request-param-name="{{field}}" data-sample-request-param-group="sample-request-param-{{@../index}}">
<input id="sample-request-param-field-{{field}}" type="text" placeholder="{{field}}" class="form-control sample-request-param" data-sample-request-param-name="{{field}}" data-sample-request-param-group="sample-request-param-{{@../index}}" {{#if optional}}data-sample-request-param-optional="true"{{/if}}>
<div class="input-group-addon">{{{type}}}</div>
</div>
{{/each}}

View File

@ -9,6 +9,8 @@ define([
'./locales/pt_br.js',
'./locales/ro.js',
'./locales/ru.js',
'./locales/tr.js',
'./locales/vi.js',
'./locales/zh.js',
'./locales/zh_cn.js'
], function() {

25
doc/locales/tr.js Normal file
View File

@ -0,0 +1,25 @@
define({
tr: {
'Allowed values:' : 'İzin verilen değerler:',
'Compare all with predecessor': 'Tümünü öncekiler ile karşılaştır',
'compare changes to:' : 'değişiklikleri karşılaştır:',
'compared to' : 'karşılaştır',
'Default value:' : 'Varsayılan değer:',
'Description' : 'Açıklama',
'Field' : 'Alan',
'General' : 'Genel',
'Generated with' : 'Oluşturan',
'Name' : 'İsim',
'No response values.' : 'Dönüş verisi yok.',
'optional' : 'opsiyonel',
'Parameter' : 'Parametre',
'Permission:' : 'İzin:',
'Response' : 'Dönüş',
'Send' : 'Gönder',
'Send a Sample Request' : 'Örnek istek gönder',
'show up to version:' : 'bu versiyona kadar göster:',
'Size range:' : 'Boyut aralığı:',
'Type' : 'Tip',
'url' : 'url'
}
});

25
doc/locales/vi.js Normal file
View File

@ -0,0 +1,25 @@
define({
vi: {
'Allowed values:' : 'Giá trị chấp nhận:',
'Compare all with predecessor': 'So sánh với tất cả phiên bản trước',
'compare changes to:' : 'so sánh sự thay đổi với:',
'compared to' : 'so sánh với',
'Default value:' : 'Giá trị mặc định:',
'Description' : 'Chú thích',
'Field' : 'Trường dữ liệu',
'General' : 'Tổng quan',
'Generated with' : 'Được tạo bởi',
'Name' : 'Tên',
'No response values.' : 'Không có kết quả trả về.',
'optional' : 'Tùy chọn',
'Parameter' : 'Tham số',
'Permission:' : 'Quyền hạn:',
'Response' : 'Kết quả',
'Send' : 'Gửi',
'Send a Sample Request' : 'Gửi một yêu cầu mẫu',
'show up to version:' : 'hiển thị phiên bản:',
'Size range:' : 'Kích cỡ:',
'Type' : 'Kiểu',
'url' : 'liên kết'
}
});

View File

@ -50,7 +50,9 @@ define([
var paramType = {};
$root.find(".sample-request-param:checked").each(function(i, element) {
var group = $(element).data("sample-request-param-group-id");
$root.find("[data-sample-request-param-group=\"" + group + "\"]").each(function(i, element) {
$root.find("[data-sample-request-param-group=\"" + group + "\"]").not(function(){
return $(this).val() == "" && $(this).is("[data-sample-request-param-optional='true']");
}).each(function(i, element) {
var key = $(element).data("sample-request-param-name");
var value = element.value;
if ( ! element.optional && element.defaultValue !== '') {

View File

@ -4,8 +4,8 @@ services:
auth:
volumes:
- ./test:/usr/src/test
- ./src/views:/usr/src/views
- ./src/public_html:/usr/src/public_html
- ./dist/src/server:/usr/src
- ./node_modules:/usr/src/node_modules
- ./config.yml:/etc/auth-server/config.yml:ro
ldap-admin:

View File

@ -25,7 +25,7 @@ dn: cn=john,ou=users,dc=example,dc=com
cn: john
objectclass: inetOrgPerson
objectclass: top
mail: john.doe@example.com
mail: clement.michaud34@gmail.com
sn: John Doe
userpassword: {SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g=

View File

@ -30,10 +30,6 @@ http {
ssl_certificate /etc/ssl/server.crt;
ssl_certificate_key /etc/ssl/server.key;
error_page 401 = @error401;
location @error401 {
return 302 https://auth.test.local:8080/login?redirect=$scheme://$http_host$request_uri;
}
location / {
proxy_set_header X-Original-URI $request_uri;
@ -41,18 +37,12 @@ http {
proxy_set_header X-Real-IP $remote_addr;
proxy_pass http://auth/;
}
location /js/ {
proxy_pass http://auth/js/;
}
proxy_intercept_errors on;
location /img/ {
proxy_pass http://auth/img/;
}
location /css/ {
proxy_pass http://auth/css/;
error_page 401 = /error/401;
error_page 403 = /error/403;
error_page 404 = /error/404;
}
}
@ -61,8 +51,7 @@ http {
root /usr/share/nginx/html;
server_name secret1.test.local secret2.test.local secret.test.local
home.test.local mx1.mail.test.local mx2.mail.test.local
localhost;
home.test.local mx1.mail.test.local mx2.mail.test.local;
ssl on;
ssl_certificate /etc/ssl/server.crt;
@ -70,7 +59,7 @@ http {
error_page 401 = @error401;
location @error401 {
return 302 https://auth.test.local:8080/login?redirect=$scheme://$http_host$request_uri;
return 302 https://auth.test.local:8080;
}
location /auth_verify {

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 20 KiB

BIN
images/reset_password.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

@ -1,20 +1,18 @@
{
"name": "authelia",
"version": "2.1.9",
"description": "2-factor authentication server using LDAP as 1st factor and TOTP or U2F as 2nd factor",
"description": "2FA Single Sign-On server for nginx using LDAP, TOTP and U2F",
"main": "src/index.js",
"bin": {
"authelia": "src/index.js"
},
"scripts": {
"test": "./node_modules/.bin/mocha --compilers ts:ts-node/register --recursive test/unitary",
"test-dbg": "./node_modules/.bin/mocha --debug-brk --compilers ts:ts-node/register --recursive test/unitary",
"int-test": "./node_modules/.bin/mocha --recursive test/integration",
"coverage": "./node_modules/.bin/istanbul cover _mocha -- -R spec --recursive test",
"build-ts": "tsc",
"watch-ts": "tsc -w",
"test": "./node_modules/.bin/mocha --compilers ts:ts-node/register --recursive test/client test/server",
"int-test": "./node_modules/.bin/mocha --compilers ts:ts-node/register --recursive test/integration",
"cover": "NODE_ENV=test nyc npm t",
"build": "tsc",
"tslint": "tslint -c tslint.json -p tsconfig.json",
"serve": "node dist/src/index.js"
"serve": "node dist/server/index.js"
},
"repository": {
"type": "git",
@ -29,7 +27,7 @@
"title": "Authelia API documentation"
},
"dependencies": {
"authdog": "^0.1.1",
"@types/cors": "^2.8.1",
"bluebird": "^3.4.7",
"body-parser": "^1.15.2",
"dovehash": "0.0.5",
@ -40,8 +38,10 @@
"nedb": "^1.8.0",
"nodemailer": "^2.7.0",
"object-path": "^0.11.3",
"pug": "^2.0.0-rc.2",
"randomstring": "^1.1.5",
"speakeasy": "^2.0.0",
"u2f": "^0.1.2",
"winston": "^2.3.1",
"yamljs": "^0.2.8"
},
@ -52,6 +52,8 @@
"@types/ejs": "^2.3.33",
"@types/express": "^4.0.35",
"@types/express-session": "0.0.32",
"@types/jquery": "^2.0.45",
"@types/jsdom": "^2.0.30",
"@types/ldapjs": "^1.0.0",
"@types/mocha": "^2.2.41",
"@types/mockdate": "^2.0.0",
@ -59,6 +61,7 @@
"@types/nodemailer": "^1.3.32",
"@types/object-path": "^0.9.28",
"@types/proxyquire": "^1.3.27",
"@types/query-string": "^4.3.1",
"@types/randomstring": "^1.1.5",
"@types/request": "0.0.43",
"@types/sinon": "^2.2.1",
@ -66,12 +69,25 @@
"@types/tmp": "0.0.33",
"@types/winston": "^2.3.2",
"@types/yamljs": "^0.2.30",
"apidoc": "^0.17.6",
"browserify": "^14.3.0",
"grunt": "^1.0.1",
"grunt-browserify": "^5.0.0",
"grunt-contrib-concat": "^1.0.1",
"grunt-contrib-copy": "^1.0.0",
"grunt-contrib-cssmin": "^2.2.0",
"grunt-contrib-watch": "^1.0.0",
"grunt-run": "^0.6.0",
"istanbul": "^0.4.5",
"jquery": "^3.2.1",
"js-logger": "^1.3.0",
"jsdom": "^11.0.0",
"mocha": "^3.2.0",
"mockdate": "^2.0.1",
"notifyjs-browser": "^0.4.2",
"nyc": "^10.3.2",
"proxyquire": "^1.8.0",
"query-string": "^4.3.4",
"request": "^2.79.0",
"should": "^11.1.1",
"sinon": "^1.17.6",
@ -79,6 +95,31 @@
"tmp": "0.0.31",
"ts-node": "^3.0.4",
"tslint": "^5.2.0",
"typescript": "^2.3.2"
"typescript": "^2.3.2",
"u2f-api": "0.0.9",
"uglify-es": "^3.0.15"
},
"nyc": {
"include": [
"src/*.ts",
"src/**/*.ts"
],
"exclude": [
"doc",
"src/types",
"dist",
"test"
],
"extension": [
".ts"
],
"require": [
"ts-node/register"
],
"reporter": [
"json",
"html"
],
"all": true
}
}

6
src/client/css/00-bootstrap.min.css vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,4 @@
body {
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI1NiIgaGVpZ2h0PSIxMDAiPgo8cmVjdCB3aWR0aD0iNTYiIGhlaWdodD0iMTAwIiBmaWxsPSIjRkZGRkZGIj48L3JlY3Q+CjxwYXRoIGQ9Ik0yOCA2NkwwIDUwTDAgMTZMMjggMEw1NiAxNkw1NiA1MEwyOCA2NkwyOCAxMDAiIGZpbGw9Im5vbmUiIHN0cm9rZT0iI0ZDRkNGQyIgc3Ryb2tlLXdpZHRoPSIyIj48L3BhdGg+CjxwYXRoIGQ9Ik0yOCAwTDI4IDM0TDAgNTBMMCA4NEwyOCAxMDBMNTYgODRMNTYgNTBMMjggMzQiIGZpbGw9Im5vbmUiIHN0cm9rZT0iI0ZCRkJGQiIgc3Ryb2tlLXdpZHRoPSIyIj48L3BhdGg+Cjwvc3ZnPg==");
}

101
src/client/css/02-login.css Normal file
View File

@ -0,0 +1,101 @@
.form-signin
{
padding: 15px;
margin: 0 auto;
}
.form-signin .form-signin-heading, .form-signin .checkbox
{
margin-bottom: 10px;
}
.form-signin .checkbox
{
font-weight: normal;
}
.form-signin .form-control
{
position: relative;
font-size: 16px;
height: auto;
padding: 10px;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.form-signin .form-control:focus
{
z-index: 2;
}
.form-signin input[type="text"]
{
margin-bottom: -1px;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
.form-signin input[type="password"]
{
/* margin-bottom: 10px; */
border-top-left-radius: 0;
border-top-right-radius: 0;
}
.account-wall
{
border: 1px solid #DDD;
margin-top: 20px;
padding: 20px;
padding-bottom: 40px;
background-color: #f7f7f7;
-moz-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3);
-webkit-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3);
box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3);
}
.account-wall h1
{
color: #555;
margin-bottom: 30px;
font-weight: 400;
display: block;
text-align: center;
}
.account-wall p
{
text-align: center;
margin: 10px 10px;
margin-top: 30px;
font-size: 1.3em;
}
.account-wall .form-inputs
{
margin-bottom: 10px;
}
.account-wall hr {
border-color: #c5c5c5;
}
.header-img
{
width: 96px;
height: 96px;
margin: 0 auto 10px;
display: block;
-moz-border-radius: 50%;
-webkit-border-radius: 50%;
border-radius: 50%;
}
.link
{
margin-top: 10px;
}
.btn-primary.totp
{
background-color: rgb(102, 135, 162);
}
.btn-primary.u2f
{
background-color: rgb(83, 149, 204);
}

View File

@ -0,0 +1,12 @@
.error-401 .header-img {
border-radius: 0%;
}
.error-403 .header-img {
border-radius: 0%;
}
.error-404 .header-img {
border-radius: 0%;
}

View File

@ -0,0 +1,4 @@
.password-reset-form .header-img {
border-radius: 0%;
}

View File

@ -0,0 +1,4 @@
.password-reset-request .header-img {
border-radius: 0%;
}

View File

@ -0,0 +1,12 @@
.totp-register #secret {
background-color: white;
font-size: 0.9em;
font-weight: bold;
padding: 5px;
border: 1px solid #c7c7c7;
word-wrap: break-word;
}
.totp-register #qrcode img {
margin: 20px auto;
}

View File

@ -0,0 +1,5 @@
.u2f-register img {
display: block;
margin: 20px auto;
}

View File

@ -0,0 +1,20 @@
import BluebirdPromise = require("bluebird");
import Endpoints = require("../../server/endpoints");
export function validate(username: string, password: string, $: JQueryStatic): BluebirdPromise < void> {
return new BluebirdPromise<void>(function (resolve, reject) {
$.post(Endpoints.FIRST_FACTOR_POST, {
username: username,
password: password,
})
.done(function () {
resolve();
})
.fail(function (xhr: JQueryXHR, textStatus: string) {
if (xhr.status == 401)
reject(new Error("Authetication failed. Please check your credentials"));
reject(new Error(textStatus));
});
});
}

View File

@ -0,0 +1,3 @@
export const USERNAME_FIELD_ID = "#username";
export const PASSWORD_FIELD_ID = "#password";

View File

@ -0,0 +1,39 @@
import FirstFactorValidator = require("./FirstFactorValidator");
import JSLogger = require("js-logger");
import UISelectors = require("./UISelectors");
import Endpoints = require("../../server/endpoints");
export default function (window: Window, $: JQueryStatic, firstFactorValidator: typeof FirstFactorValidator, jslogger: typeof JSLogger) {
function onFormSubmitted() {
const username: string = $(UISelectors.USERNAME_FIELD_ID).val();
const password: string = $(UISelectors.PASSWORD_FIELD_ID).val();
jslogger.debug("Form submitted");
firstFactorValidator.validate(username, password, $)
.then(onFirstFactorSuccess, onFirstFactorFailure);
return false;
}
function onFirstFactorSuccess() {
jslogger.debug("First factor validated.");
$(UISelectors.USERNAME_FIELD_ID).val("");
$(UISelectors.PASSWORD_FIELD_ID).val("");
// Redirect to second factor
window.location.href = Endpoints.SECOND_FACTOR_GET;
}
function onFirstFactorFailure(err: Error) {
jslogger.debug("First factor failed.");
$(UISelectors.PASSWORD_FIELD_ID).val("");
$.notify("Error during authentication: " + err.message, "error");
}
$(window.document).ready(function () {
jslogger.info("Enter first factor");
$("form").on("submit", onFormSubmitted);
});
}

BIN
src/client/img/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 814 B

BIN
src/client/img/mail.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

BIN
src/client/img/padlock.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

BIN
src/client/img/password.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

BIN
src/client/img/success.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

BIN
src/client/img/user.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

BIN
src/client/img/warning.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

38
src/client/index.ts Normal file
View File

@ -0,0 +1,38 @@
import FirstFactorValidator = require("./firstfactor/FirstFactorValidator");
import FirstFactor from "./firstfactor/index";
import SecondFactor from "./secondfactor/index";
import TOTPRegister from "./totp-register/totp-register";
import U2fRegister from "./u2f-register/u2f-register";
import ResetPasswordRequest from "./reset-password/reset-password-request";
import ResetPasswordForm from "./reset-password/reset-password-form";
import jslogger = require("js-logger");
import jQuery = require("jquery");
import u2fApi = require("u2f-api");
jslogger.useDefaults();
jslogger.setLevel(jslogger.INFO);
require("notifyjs-browser")(jQuery);
export = {
firstfactor: function () {
FirstFactor(window, jQuery, FirstFactorValidator, jslogger);
},
secondfactor: function () {
SecondFactor(window, jQuery, u2fApi);
},
register_totp: function() {
TOTPRegister(window, jQuery);
},
register_u2f: function () {
U2fRegister(window, jQuery);
},
reset_password_request: function () {
ResetPasswordRequest(window, jQuery);
},
reset_password_form: function () {
ResetPasswordForm(window, jQuery);
}
};

View File

@ -0,0 +1,2 @@
export const FORM_SELECTOR = ".form-signin";

View File

@ -0,0 +1,49 @@
import BluebirdPromise = require("bluebird");
import Endpoints = require("../../server/endpoints");
import Constants = require("./constants");
export default function (window: Window, $: JQueryStatic) {
function modifyPassword(newPassword: string) {
return new BluebirdPromise(function (resolve, reject) {
$.post(Endpoints.RESET_PASSWORD_FORM_POST, {
password: newPassword,
})
.done(function (data) {
resolve(data);
})
.fail(function (xhr, status) {
reject(status);
});
});
}
function onFormSubmitted() {
const password1 = $("#password1").val();
const password2 = $("#password2").val();
if (!password1 || !password2) {
$.notify("You must enter your new password twice.", "warn");
return false;
}
if (password1 != password2) {
$.notify("The passwords are different", "warn");
return false;
}
modifyPassword(password1)
.then(function () {
$.notify("Your password has been changed. Please login again", "success");
window.location.href = Endpoints.FIRST_FACTOR_GET;
})
.error(function () {
$.notify("An error occurred during password change.", "warn");
});
return false;
}
$(document).ready(function () {
$(Constants.FORM_SELECTOR).on("submit", onFormSubmitted);
});
}

View File

@ -0,0 +1,49 @@
import BluebirdPromise = require("bluebird");
import Endpoints = require("../../server/endpoints");
import Constants = require("./constants");
import jslogger = require("js-logger");
export default function(window: Window, $: JQueryStatic) {
function requestPasswordReset(username: string) {
return new BluebirdPromise(function (resolve, reject) {
$.get(Endpoints.RESET_PASSWORD_IDENTITY_START_GET, {
userid: username,
})
.done(function () {
resolve();
})
.fail(function (xhr: JQueryXHR, textStatus: string) {
reject(new Error(textStatus));
});
});
}
function onFormSubmitted() {
const username = $("#username").val();
if (!username) {
$.notify("You must provide your username to reset your password.", "warn");
return;
}
requestPasswordReset(username)
.then(function () {
$.notify("An email has been sent. Click on the link to change your password", "success");
setTimeout(function () {
window.location.replace(Endpoints.FIRST_FACTOR_GET);
}, 1000);
})
.error(function () {
$.notify("Are you sure this is your username?", "warn");
});
return false;
}
$(document).ready(function () {
jslogger.debug("Reset password request form setup");
$(Constants.FORM_SELECTOR).on("submit", onFormSubmitted);
});
}

View File

@ -0,0 +1,22 @@
import BluebirdPromise = require("bluebird");
import Endpoints = require("../../server/endpoints");
export function validate(token: string, $: JQueryStatic): BluebirdPromise<string> {
return new BluebirdPromise<string>(function (resolve, reject) {
$.ajax({
url: Endpoints.SECOND_FACTOR_TOTP_POST,
data: {
token: token,
},
method: "POST",
dataType: "json"
} as JQueryAjaxSettings)
.done(function (data: any) {
resolve(data);
})
.fail(function (xhr: JQueryXHR, textStatus: string) {
reject(new Error(textStatus));
});
});
}

View File

@ -0,0 +1,61 @@
import U2fApi = require("u2f-api");
import U2f = require("u2f");
import BluebirdPromise = require("bluebird");
import { SignMessage } from "../../server/lib/routes/secondfactor/u2f/sign_request/SignMessage";
import Endpoints = require("../../server/endpoints");
function finishU2fAuthentication(responseData: U2fApi.SignResponse, $: JQueryStatic): BluebirdPromise<void> {
return new BluebirdPromise<void>(function (resolve, reject) {
$.ajax({
url: Endpoints.SECOND_FACTOR_U2F_SIGN_POST,
data: responseData,
method: "POST",
dataType: "json"
} as JQueryAjaxSettings)
.done(function (data) {
resolve(data);
})
.fail(function (xhr: JQueryXHR, textStatus: string) {
reject(new Error(textStatus));
});
});
}
function startU2fAuthentication($: JQueryStatic, u2fApi: typeof U2fApi): BluebirdPromise<void> {
return new BluebirdPromise<void>(function (resolve, reject) {
$.get(Endpoints.SECOND_FACTOR_U2F_SIGN_REQUEST_GET, {}, undefined, "json")
.done(function (signResponse: SignMessage) {
$.notify("Please touch the token", "info");
const signRequest: U2fApi.SignRequest = {
appId: signResponse.request.appId,
challenge: signResponse.request.challenge,
keyHandle: signResponse.keyHandle, // linked to the client session cookie
version: "U2F_V2"
};
u2fApi.sign([signRequest], 60)
.then(function (signResponse: U2fApi.SignResponse) {
finishU2fAuthentication(signResponse, $)
.then(function (data) {
resolve(data);
}, function (err) {
$.notify("Error when finish U2F transaction", "error");
reject(err);
});
})
.catch(function (err: Error) {
reject(err);
});
})
.fail(function (xhr: JQueryXHR, textStatus: string) {
reject(new Error(textStatus));
});
});
}
export function validate($: JQueryStatic, u2fApi: typeof U2fApi): BluebirdPromise<void> {
return startU2fAuthentication($, u2fApi);
}

View File

@ -0,0 +1,5 @@
export const TOTP_FORM_SELECTOR = ".form-signin.totp";
export const TOTP_TOKEN_SELECTOR = ".form-signin #token";
export const U2F_FORM_SELECTOR = ".form-signin.u2f";

View File

@ -0,0 +1,57 @@
import U2fApi = require("u2f-api");
import jslogger = require("js-logger");
import TOTPValidator = require("./TOTPValidator");
import U2FValidator = require("./U2FValidator");
import Endpoints = require("../../server/endpoints");
import Constants = require("./constants");
export default function (window: Window, $: JQueryStatic, u2fApi: typeof U2fApi) {
function onAuthenticationSuccess(data: any) {
window.location.href = data.redirection_url;
}
function onSecondFactorTotpSuccess(data: any) {
onAuthenticationSuccess(data);
}
function onSecondFactorTotpFailure(err: Error) {
$.notify("Error while validating TOTP token. Cause: " + err.message, "error");
}
function onU2fAuthenticationSuccess(data: any) {
onAuthenticationSuccess(data);
}
function onU2fAuthenticationFailure() {
$.notify("Problem with U2F authentication. Did you register before authenticating?", "warn");
}
function onTOTPFormSubmitted(): boolean {
const token = $(Constants.TOTP_TOKEN_SELECTOR).val();
jslogger.debug("TOTP token is %s", token);
TOTPValidator.validate(token, $)
.then(onSecondFactorTotpSuccess)
.catch(onSecondFactorTotpFailure);
return false;
}
function onU2FFormSubmitted(): boolean {
jslogger.debug("Start U2F authentication");
U2FValidator.validate($, U2fApi)
.then(onU2fAuthenticationSuccess, onU2fAuthenticationFailure);
return false;
}
$(window.document).ready(function () {
$(Constants.TOTP_FORM_SELECTOR).on("submit", onTOTPFormSubmitted);
$(Constants.U2F_FORM_SELECTOR).on("submit", onU2FFormSubmitted);
});
}

View File

@ -0,0 +1,11 @@
import jslogger = require("js-logger");
import UISelector = require("./ui-selector");
export default function(window: Window, $: JQueryStatic) {
jslogger.debug("Creating QRCode from OTPAuth url");
const qrcode = $(UISelector.QRCODE_ID_SELECTOR);
const val = qrcode.text();
qrcode.empty();
new (window as any).QRCode(qrcode.get(0), val);
}

View File

@ -0,0 +1,2 @@
export const QRCODE_ID_SELECTOR = "#qrcode";

View File

@ -0,0 +1,53 @@
import BluebirdPromise = require("bluebird");
import U2f = require("u2f");
import u2fApi = require("u2f-api");
import Endpoints = require("../../server/endpoints");
import jslogger = require("js-logger");
export default function(window: Window, $: JQueryStatic) {
function checkRegistration(regResponse: u2fApi.RegisterResponse, fn: (err: Error) => void) {
const registrationData: U2f.RegistrationData = regResponse;
jslogger.debug("registrationResponse = %s", JSON.stringify(registrationData));
$.post(Endpoints.SECOND_FACTOR_U2F_REGISTER_POST, registrationData, undefined, "json")
.done(function (data) {
document.location.href = data.redirection_url;
})
.fail(function (xhr, status) {
$.notify("Error when finish U2F transaction" + status);
});
}
function requestRegistration(fn: (err: Error) => void) {
$.get(Endpoints.SECOND_FACTOR_U2F_REGISTER_REQUEST_GET, {}, undefined, "json")
.done(function (registrationRequest: U2f.Request) {
jslogger.debug("registrationRequest = %s", JSON.stringify(registrationRequest));
const registerRequest: u2fApi.RegisterRequest = registrationRequest;
u2fApi.register([registerRequest], [], 120)
.then(function (res: u2fApi.RegisterResponse) {
checkRegistration(res, fn);
})
.catch(function (err: Error) {
fn(err);
});
});
}
function onRegisterFailure(err: Error) {
$.notify("Problem authenticating with U2F.", "error");
}
$(document).ready(function () {
requestRegistration(function (err: Error) {
if (err) {
onRegisterFailure(err);
return;
}
});
});
}

View File

@ -1,156 +0,0 @@
import objectPath = require("object-path");
import randomstring = require("randomstring");
import BluebirdPromise = require("bluebird");
import util = require("util");
import exceptions = require("./Exceptions");
import fs = require("fs");
import ejs = require("ejs");
import UserDataStore from "./UserDataStore";
import { ILogger } from "../types/ILogger";
import express = require("express");
import Identity = require("../types/Identity");
import { IdentityValidationRequestContent } from "./UserDataStore";
const filePath = __dirname + "/../resources/email-template.ejs";
const email_template = fs.readFileSync(filePath, "utf8");
// IdentityValidator allows user to go through a identity validation process in two steps:
// - Request an operation to be performed (password reset, registration).
// - Confirm operation with email.
export interface IdentityValidable {
challenge(): string;
templateName(): string;
preValidation(req: express.Request): BluebirdPromise<Identity.Identity>;
mailSubject(): string;
}
export class IdentityValidator {
private userDataStore: UserDataStore;
private logger: ILogger;
constructor(userDataStore: UserDataStore, logger: ILogger) {
this.userDataStore = userDataStore;
this.logger = logger;
}
static setup(app: express.Application, endpoint: string, handler: IdentityValidable, userDataStore: UserDataStore, logger: ILogger) {
const identityValidator = new IdentityValidator(userDataStore, logger);
app.get(endpoint, identityValidator.identity_check_get(endpoint, handler));
app.post(endpoint, identityValidator.identity_check_post(endpoint, handler));
}
private issue_token(userid: string, content: Object): BluebirdPromise<string> {
const five_minutes = 4 * 60 * 1000;
const token = randomstring.generate({ length: 64 });
const that = this;
this.logger.debug("identity_check: issue identity token %s for 5 minutes", token);
return this.userDataStore.issue_identity_check_token(userid, token, content, five_minutes)
.then(function () {
return BluebirdPromise.resolve(token);
});
}
private consume_token(token: string): BluebirdPromise<IdentityValidationRequestContent> {
this.logger.debug("identity_check: consume token %s", token);
return this.userDataStore.consume_identity_check_token(token);
}
private identity_check_get(endpoint: string, handler: IdentityValidable): express.RequestHandler {
const that = this;
return function (req: express.Request, res: express.Response) {
const logger = req.app.get("logger");
const identity_token = objectPath.get<express.Request, string>(req, "query.identity_token");
logger.info("GET identity_check: identity token provided is %s", identity_token);
if (!identity_token) {
res.status(403);
res.send();
return;
}
that.consume_token(identity_token)
.then(function (content: IdentityValidationRequestContent) {
objectPath.set(req, "session.auth_session.identity_check", {});
req.session.auth_session.identity_check.challenge = handler.challenge();
req.session.auth_session.identity_check.userid = content.userid;
res.render(handler.templateName());
}, function (err: Error) {
logger.error("GET identity_check: Error while consuming token %s", err);
throw new exceptions.AccessDeniedError("Access denied");
})
.catch(exceptions.AccessDeniedError, function (err: Error) {
logger.error("GET identity_check: Access Denied %s", err);
res.status(403);
res.send();
})
.catch(function (err: Error) {
logger.error("GET identity_check: Internal error %s", err);
res.status(500);
res.send();
});
};
}
private identity_check_post(endpoint: string, handler: IdentityValidable): express.RequestHandler {
const that = this;
return function (req: express.Request, res: express.Response) {
const logger = req.app.get("logger");
const notifier = req.app.get("notifier");
let identity: Identity.Identity;
handler.preValidation(req)
.then(function (id: Identity.Identity) {
identity = id;
const email_address = objectPath.get<Identity.Identity, string>(identity, "email");
const userid = objectPath.get<Identity.Identity, string>(identity, "userid");
if (!(email_address && userid)) {
throw new exceptions.IdentityError("Missing user id or email address");
}
return that.issue_token(userid, undefined);
}, function (err: Error) {
throw new exceptions.AccessDeniedError(err.message);
})
.then(function (token: string) {
const redirect_url = objectPath.get<express.Request, string>(req, "body.redirect");
const original_uri = objectPath.get<express.Request, string>(req, "headers.x-original-uri", "");
const original_url = util.format("https://%s%s", req.headers.host, original_uri);
let link_url = util.format("%s?identity_token=%s", original_url, token);
if (redirect_url) {
link_url = util.format("%s&redirect=%s", link_url, redirect_url);
}
logger.info("POST identity_check: notify to %s", identity.userid);
return notifier.notify(identity, handler.mailSubject(), link_url);
})
.then(function () {
res.status(204);
res.send();
})
.catch(exceptions.IdentityError, function (err: Error) {
logger.error("POST identity_check: %s", err);
res.status(400);
res.send();
})
.catch(exceptions.AccessDeniedError, function (err: Error) {
logger.error("POST identity_check: %s", err);
res.status(403);
res.send();
})
.catch(function (err: Error) {
logger.error("POST identity_check: Error %s", err);
res.status(500);
res.send();
});
};
}
}

View File

@ -1,282 +0,0 @@
import express = require("express");
import routes = require("./routes");
import IdentityValidator = require("./IdentityValidator");
import UserDataStore from "./UserDataStore";
import { ILogger } from "../types/ILogger";
export default class RestApi {
static setup(app: express.Application, userDataStore: UserDataStore, logger: ILogger): void {
/**
* @apiDefine UserSession
* @apiHeader {String} Cookie Cookie containing "connect.sid", the user
* session token.
*/
/**
* @apiDefine InternalError
* @apiError (Error 500) {String} error Internal error message.
*/
/**
* @apiDefine IdentityValidationPost
*
* @apiSuccess (Success 204) status Identity validation has been initiated.
* @apiError (Error 403) AccessDenied Access is denied.
* @apiError (Error 400) InvalidIdentity User identity is invalid.
* @apiError (Error 500) {String} error Internal error message.
*
* @apiDescription This request issue an identity validation token for the user
* bound to the session. It sends a challenge to the email address set in the user
* LDAP entry. The user must visit the sent URL to complete the validation and
* continue the registration process.
*/
/**
* @apiDefine IdentityValidationGet
* @apiParam {String} identity_token The one-time identity validation token provided in the email.
* @apiSuccess (Success 200) {String} content The content of the page.
* @apiError (Error 403) AccessDenied Access is denied.
* @apiError (Error 500) {String} error Internal error message.
*/
/**
* @api {get} /login Serve login page
* @apiName Login
* @apiGroup Pages
* @apiVersion 1.0.0
*
* @apiParam {String} redirect Redirect to this URL when user is authenticated.
* @apiSuccess (Success 200) {String} Content The content of the login page.
*
* @apiDescription Create a user session and serve the login page along with
* a cookie.
*/
app.get("/login", routes.login);
/**
* @api {get} /logout Server logout page
* @apiName Logout
* @apiGroup Pages
* @apiVersion 1.0.0
*
* @apiParam {String} redirect Redirect to this URL when user is deauthenticated.
* @apiSuccess (Success 301) redirect Redirect to the URL.
*
* @apiDescription Deauthenticate the user and redirect him.
*/
app.get("/logout", routes.logout);
/**
* @api {post} /totp-register Request TOTP registration
* @apiName RequestTOTPRegistration
* @apiGroup Registration
* @apiVersion 1.0.0
* @apiUse UserSession
* @apiUse IdentityValidationPost
*/
/**
* @api {get} /totp-register Serve TOTP registration page
* @apiName ServeTOTPRegistrationPage
* @apiGroup Registration
* @apiVersion 1.0.0
* @apiUse UserSession
* @apiUse IdentityValidationGet
*
*
* @apiDescription Serves the TOTP registration page that displays the secret.
* The secret is a QRCode and a base32 secret.
*/
IdentityValidator.IdentityValidator.setup(app, "/totp-register", routes.totp_register.icheck_interface, userDataStore, logger);
/**
* @api {post} /u2f-register Request U2F registration
* @apiName RequestU2FRegistration
* @apiGroup Registration
* @apiVersion 1.0.0
* @apiUse UserSession
* @apiUse IdentityValidationPost
*/
/**
* @api {get} /u2f-register Serve U2F registration page
* @apiName ServeU2FRegistrationPage
* @apiGroup Pages
* @apiVersion 1.0.0
* @apiUse UserSession
* @apiUse IdentityValidationGet
*
* @apiDescription Serves the U2F registration page that asks the user to
* touch the token of the U2F device.
*/
IdentityValidator.IdentityValidator.setup(app, "/u2f-register", routes.u2f_register.icheck_interface, userDataStore, logger);
/**
* @api {post} /reset-password Request for password reset
* @apiName RequestPasswordReset
* @apiGroup Registration
* @apiVersion 1.0.0
* @apiUse UserSession
* @apiUse IdentityValidationPost
*/
/**
* @api {get} /reset-password Serve password reset form.
* @apiName ServePasswordResetForm
* @apiGroup Pages
* @apiVersion 1.0.0
* @apiUse UserSession
* @apiUse IdentityValidationGet
*
* @apiDescription Serves password reset form that allow the user to provide
* the new password.
*/
IdentityValidator.IdentityValidator.setup(app, "/reset-password", routes.reset_password.icheck_interface, userDataStore, logger);
app.get("/reset-password-form", function (req, res) { res.render("reset-password-form"); });
/**
* @api {post} /new-password Set LDAP password
* @apiName SetLDAPPassword
* @apiGroup Registration
* @apiVersion 1.0.0
* @apiUse UserSession
*
* @apiParam {String} password New password
*
* @apiDescription Set a new password for the user.
*/
app.post("/new-password", routes.reset_password.post);
/**
* @api {post} /new-totp-secret Generate TOTP secret
* @apiName GenerateTOTPSecret
* @apiGroup Registration
* @apiVersion 1.0.0
* @apiUse UserSession
*
* @apiSuccess (Success 200) {String} base32 The base32 representation of the secret.
* @apiSuccess (Success 200) {String} ascii The ASCII representation of the secret.
* @apiSuccess (Success 200) {String} qrcode The QRCode of the secret in URI format.
*
* @apiError (Error 403) {String} error No user provided in the session or
* unexpected identity validation challenge in the session.
* @apiError (Error 500) {String} error Internal error message
*
* @apiDescription Generate a new TOTP secret and returns it.
*/
app.post("/new-totp-secret", routes.totp_register.post);
/**
* @api {get} /verify Verify user authentication
* @apiName VerifyAuthentication
* @apiGroup Verification
* @apiVersion 1.0.0
* @apiUse UserSession
*
* @apiSuccess (Success 204) status The user is authenticated.
* @apiError (Error 401) status The user is not authenticated.
*
* @apiDescription Verify that the user is authenticated, i.e., the two
* factors have been validated
*/
app.get("/verify", routes.verify);
/**
* @api {post} /1stfactor LDAP authentication
* @apiName ValidateFirstFactor
* @apiGroup Authentication
* @apiVersion 1.0.0
* @apiUse UserSession
* @apiUse InternalError
*
* @apiParam {String} username User username.
* @apiParam {String} password User password.
*
* @apiSuccess (Success 204) status 1st factor is validated.
* @apiError (Error 401) {none} error 1st factor is not validated.
* @apiError (Error 403) {none} error Access has been restricted after too
* many authentication attempts
*
* @apiDescription Verify credentials against the LDAP.
*/
app.post("/1stfactor", routes.first_factor);
/**
* @api {post} /2ndfactor/totp TOTP authentication
* @apiName ValidateTOTPSecondFactor
* @apiGroup Authentication
* @apiVersion 1.0.0
* @apiUse UserSession
* @apiUse InternalError
*
* @apiParam {String} token TOTP token.
*
* @apiSuccess (Success 204) status TOTP token is valid.
* @apiError (Error 401) {none} error TOTP token is invalid.
*
* @apiDescription Verify TOTP token. The user is authenticated upon success.
*/
app.post("/2ndfactor/totp", routes.second_factor.totp);
/**
* @api {get} /2ndfactor/u2f/sign_request U2F Start authentication
* @apiName StartU2FAuthentication
* @apiGroup Authentication
* @apiVersion 1.0.0
* @apiUse UserSession
* @apiUse InternalError
*
* @apiSuccess (Success 200) authentication_request The U2F authentication request.
* @apiError (Error 401) {none} error There is no key registered for user in session.
*
* @apiDescription Initiate an authentication request using a U2F device.
*/
app.get("/2ndfactor/u2f/sign_request", routes.second_factor.u2f.sign_request);
/**
* @api {post} /2ndfactor/u2f/sign U2F Complete authentication
* @apiName CompleteU2FAuthentication
* @apiGroup Authentication
* @apiVersion 1.0.0
* @apiUse UserSession
* @apiUse InternalError
*
* @apiSuccess (Success 204) status The U2F authentication succeeded.
* @apiError (Error 403) {none} error No authentication request has been provided.
*
* @apiDescription Complete authentication request of the U2F device.
*/
app.post("/2ndfactor/u2f/sign", routes.second_factor.u2f.sign);
/**
* @api {get} /2ndfactor/u2f/register_request U2F Start device registration
* @apiName StartU2FRegistration
* @apiGroup Registration
* @apiVersion 1.0.0
* @apiUse UserSession
* @apiUse InternalError
*
* @apiSuccess (Success 200) authentication_request The U2F registration request.
* @apiError (Error 403) {none} error Unexpected identity validation challenge.
*
* @apiDescription Initiate a U2F device registration request.
*/
app.get("/2ndfactor/u2f/register_request", routes.second_factor.u2f.register_request);
/**
* @api {post} /2ndfactor/u2f/register U2F Complete device registration
* @apiName CompleteU2FRegistration
* @apiGroup Registration
* @apiVersion 1.0.0
* @apiUse UserSession
* @apiUse InternalError
*
* @apiSuccess (Success 204) status The U2F registration succeeded.
* @apiError (Error 403) {none} error Unexpected identity validation challenge.
* @apiError (Error 403) {none} error No registration request has been provided.
*
* @apiDescription Complete U2F registration request.
*/
app.post("/2ndfactor/u2f/register", routes.second_factor.u2f.register);
}
}

View File

@ -1,94 +0,0 @@
import { UserConfiguration } from "./Configuration";
import { GlobalDependencies } from "../types/Dependencies";
import AuthenticationRegulator from "./AuthenticationRegulator";
import UserDataStore from "./UserDataStore";
import ConfigurationAdapter from "./ConfigurationAdapter";
import { NotifierFactory } from "./notifiers/NotifierFactory";
import TOTPValidator from "./TOTPValidator";
import TOTPGenerator from "./TOTPGenerator";
import RestApi from "./RestApi";
import { LdapClient } from "./LdapClient";
import BluebirdPromise = require("bluebird");
import { IdentityValidator } from "./IdentityValidator";
import * as Express from "express";
import * as BodyParser from "body-parser";
import * as Path from "path";
import * as http from "http";
import AccessController from "./access_control/AccessController";
export default class Server {
private httpServer: http.Server;
start(yaml_configuration: UserConfiguration, deps: GlobalDependencies): BluebirdPromise<void> {
const config = ConfigurationAdapter.adapt(yaml_configuration);
const view_directory = Path.resolve(__dirname, "../views");
const public_html_directory = Path.resolve(__dirname, "../public_html");
const datastore_options = {
directory: config.store_directory,
inMemory: config.store_in_memory
};
const app = Express();
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(deps.session({
secret: config.session.secret,
resave: false,
saveUninitialized: true,
cookie: {
secure: false,
maxAge: config.session.expiration,
domain: config.session.domain
},
}));
app.set("views", view_directory);
app.set("view engine", "ejs");
// by default the level of logs is info
deps.winston.level = config.logs_level || "info";
const five_minutes = 5 * 60;
const userDataStore = new UserDataStore(datastore_options, deps.nedb);
const regulator = new AuthenticationRegulator(userDataStore, five_minutes);
const notifier = NotifierFactory.build(config.notifier, deps.nodemailer);
const ldap = new LdapClient(config.ldap, deps.ldapjs, deps.winston);
const accessController = new AccessController(config.access_control, deps.winston);
const totpValidator = new TOTPValidator(deps.speakeasy);
const totpGenerator = new TOTPGenerator(deps.speakeasy);
const identityValidator = new IdentityValidator(userDataStore, deps.winston);
app.set("logger", deps.winston);
app.set("ldap", ldap);
app.set("totp validator", totpValidator);
app.set("totp generator", totpGenerator);
app.set("u2f", deps.u2f);
app.set("user data store", userDataStore);
app.set("notifier", notifier);
app.set("authentication regulator", regulator);
app.set("config", config);
app.set("access controller", accessController);
app.set("identity validator", identityValidator);
RestApi.setup(app, userDataStore, deps.winston);
return new BluebirdPromise<void>((resolve, reject) => {
this.httpServer = app.listen(config.port, function (err: string) {
console.log("Listening on %d...", config.port);
resolve();
});
});
}
stop() {
this.httpServer.close();
}
}

View File

@ -1,41 +0,0 @@
import FirstFactor = require("./routes/FirstFactor");
import SecondFactorRoutes = require("./routes/SecondFactorRoutes");
import PasswordReset = require("./routes/PasswordReset");
import AuthenticationValidator = require("./routes/AuthenticationValidator");
import U2FRegistration = require("./routes/U2FRegistration");
import TOTPRegistration = require("./routes/TOTPRegistration");
import objectPath = require("object-path");
import express = require("express");
export = {
login: serveLogin,
logout: serveLogout,
verify: AuthenticationValidator,
first_factor: FirstFactor,
second_factor: SecondFactorRoutes,
reset_password: PasswordReset,
u2f_register: U2FRegistration,
totp_register: TOTPRegistration,
};
function serveLogin(req: express.Request, res: express.Response) {
if (!(objectPath.has(req, "session.auth_session"))) {
req.session.auth_session = {};
req.session.auth_session.first_factor = false;
req.session.auth_session.second_factor = false;
}
res.render("login");
}
function serveLogout(req: express.Request, res: express.Response) {
const redirect_param = req.query.redirect;
const redirect_url = redirect_param || "/";
req.session.auth_session = {
first_factor: false,
second_factor: false
};
res.redirect(redirect_url);
}

View File

@ -1,53 +0,0 @@
import objectPath = require("object-path");
import BluebirdPromise = require("bluebird");
import express = require("express");
import AccessController from "../access_control/AccessController";
import exceptions = require("../Exceptions");
function verify_filter(req: express.Request, res: express.Response) {
const logger = req.app.get("logger");
const accessController: AccessController = req.app.get("access controller");
if (!objectPath.has(req, "session.auth_session"))
return BluebirdPromise.reject("No auth_session variable");
if (!objectPath.has(req, "session.auth_session.first_factor"))
return BluebirdPromise.reject("No first factor variable");
if (!objectPath.has(req, "session.auth_session.second_factor"))
return BluebirdPromise.reject("No second factor variable");
if (!objectPath.has(req, "session.auth_session.userid"))
return BluebirdPromise.reject("No userid variable");
const username = objectPath.get<express.Request, string>(req, "session.auth_session.userid");
const groups = objectPath.get<express.Request, string[]>(req, "session.auth_session.groups");
const host = objectPath.get<express.Request, string>(req, "headers.host");
const domain = host.split(":")[0];
const isAllowed = accessController.isDomainAllowedForUser(domain, username, groups);
if (!isAllowed) return BluebirdPromise.reject(
new exceptions.DomainAccessDenied("User '" + username + "' does not have access to " + domain));
if (!req.session.auth_session.first_factor ||
!req.session.auth_session.second_factor)
return BluebirdPromise.reject(new exceptions.AccessDeniedError("First or second factor not validated"));
return BluebirdPromise.resolve();
}
export = function (req: express.Request, res: express.Response) {
verify_filter(req, res)
.then(function () {
res.status(204);
res.send();
})
.catch(function (err) {
req.app.get("logger").error(err);
res.status(401);
res.send();
});
};

View File

@ -1,19 +0,0 @@
import objectPath = require("object-path");
import express = require("express");
type ExpressRequest = (req: express.Request, res: express.Response, next?: express.NextFunction) => void;
export = function(callback: ExpressRequest): ExpressRequest {
return function (req: express.Request, res: express.Response, next: express.NextFunction) {
const auth_session = req.session.auth_session;
const first_factor = objectPath.has(req, "session.auth_session.first_factor")
&& req.session.auth_session.first_factor;
if (!first_factor) {
res.status(403);
res.send();
return;
}
callback(req, res, next);
};
};

View File

@ -1,82 +0,0 @@
import exceptions = require("../Exceptions");
import objectPath = require("object-path");
import BluebirdPromise = require("bluebird");
import express = require("express");
import AccessController from "../access_control/AccessController";
import AuthenticationRegulator from "../AuthenticationRegulator";
import { LdapClient } from "../LdapClient";
export = function (req: express.Request, res: express.Response) {
const username: string = req.body.username;
const password: string = req.body.password;
if (!username || !password) {
res.status(401);
res.send();
return;
}
const logger = req.app.get("logger");
const ldap: LdapClient = req.app.get("ldap");
const config = req.app.get("config");
const regulator: AuthenticationRegulator = req.app.get("authentication regulator");
const accessController: AccessController = req.app.get("access controller");
logger.info("1st factor: Starting authentication of user \"%s\"", username);
logger.debug("1st factor: Start bind operation against LDAP");
logger.debug("1st factor: username=%s", username);
regulator.regulate(username)
.then(function () {
return ldap.bind(username, password);
})
.then(function () {
objectPath.set(req, "session.auth_session.userid", username);
objectPath.set(req, "session.auth_session.first_factor", true);
logger.info("1st factor: LDAP binding successful");
logger.debug("1st factor: Retrieve email from LDAP");
return BluebirdPromise.join(ldap.get_emails(username), ldap.get_groups(username));
})
.then(function (data: [string[], string[]]) {
const emails: string[] = data[0];
const groups: string[] = data[1];
if (!emails && emails.length <= 0) throw new Error("No email found");
logger.debug("1st factor: Retrieved email are %s", emails);
objectPath.set(req, "session.auth_session.email", emails[0]);
objectPath.set(req, "session.auth_session.groups", groups);
regulator.mark(username, true);
res.status(204);
res.send();
})
.catch(exceptions.LdapSeachError, function (err: Error) {
logger.error("1st factor: Unable to retrieve email from LDAP", err);
res.status(500);
res.send();
})
.catch(exceptions.LdapBindError, function (err: Error) {
logger.error("1st factor: LDAP binding failed");
logger.debug("1st factor: LDAP binding failed due to ", err);
regulator.mark(username, false);
res.status(401);
res.send("Bad credentials");
})
.catch(exceptions.AuthenticationRegulationError, function (err: Error) {
logger.error("1st factor: the regulator rejected the authentication of user %s", username);
logger.debug("1st factor: authentication rejected due to %s", err);
res.status(403);
res.send("Access has been restricted for a few minutes...");
})
.catch(exceptions.DomainAccessDenied, (err: Error) => {
logger.error("1st factor: ", err);
res.status(401);
res.send("Access denied...");
})
.catch(function (err: Error) {
console.log(err.stack);
logger.error("1st factor: Unhandled error %s", err);
res.status(500);
res.send("Internal error");
});
};

View File

@ -1,81 +0,0 @@
import BluebirdPromise = require("bluebird");
import objectPath = require("object-path");
import exceptions = require("../Exceptions");
import express = require("express");
import { Identity } from "../../types/Identity";
import { IdentityValidable } from "../IdentityValidator";
const CHALLENGE = "reset-password";
class PasswordResetHandler implements IdentityValidable {
challenge(): string {
return CHALLENGE;
}
templateName(): string {
return "reset-password";
}
preValidation(req: express.Request): BluebirdPromise<Identity> {
const userid = objectPath.get(req, "body.userid");
if (!userid) {
return BluebirdPromise.reject(new exceptions.AccessDeniedError("No user id provided"));
}
const ldap = req.app.get("ldap");
return ldap.get_emails(userid)
.then(function (emails: string[]) {
if (!emails && emails.length <= 0) throw new Error("No email found");
const identity = {
email: emails[0],
userid: userid
};
return BluebirdPromise.resolve(identity);
});
}
mailSubject(): string {
return "Reset your password";
}
}
function protect(fn: express.RequestHandler) {
return function (req: express.Request, res: express.Response) {
const challenge = objectPath.get(req, "session.auth_session.identity_check.challenge");
if (challenge != CHALLENGE) {
res.status(403);
res.send();
return;
}
fn(req, res, undefined);
};
}
function post(req: express.Request, res: express.Response) {
const logger = req.app.get("logger");
const ldap = req.app.get("ldap");
const new_password = objectPath.get(req, "body.password");
const userid = objectPath.get(req, "session.auth_session.identity_check.userid");
logger.info("POST reset-password: User %s wants to reset his/her password", userid);
ldap.update_password(userid, new_password)
.then(function () {
logger.info("POST reset-password: Password reset for user %s", userid);
objectPath.set(req, "session.auth_session", undefined);
res.status(204);
res.send();
})
.catch(function (err: Error) {
logger.error("POST reset-password: Error while resetting the password of user %s. %s", userid, err);
res.status(500);
res.send();
});
}
export = {
icheck_interface: new PasswordResetHandler(),
post: protect(post)
};

View File

@ -1,28 +0,0 @@
import DenyNotLogged = require("./DenyNotLogged");
import U2FRoutes = require("./U2FRoutes");
import TOTPAuthenticator = require("./TOTPAuthenticator");
import express = require("express");
interface SecondFactorRoutes {
totp: express.RequestHandler;
u2f: {
register_request: express.RequestHandler;
register: express.RequestHandler;
sign_request: express.RequestHandler;
sign: express.RequestHandler;
};
}
export = {
totp: DenyNotLogged(TOTPAuthenticator),
u2f: {
register_request: U2FRoutes.register_request,
register: U2FRoutes.register,
sign_request: DenyNotLogged(U2FRoutes.sign_request),
sign: DenyNotLogged(U2FRoutes.sign),
}
} as SecondFactorRoutes;

View File

@ -1,49 +0,0 @@
import exceptions = require("../Exceptions");
import objectPath = require("object-path");
import express = require("express");
import { TOTPSecretDocument } from "../UserDataStore";
import BluebirdPromise = require("bluebird");
const UNAUTHORIZED_MESSAGE = "Unauthorized access";
export = function(req: express.Request, res: express.Response) {
const logger = req.app.get("logger");
const userid = objectPath.get(req, "session.auth_session.userid");
logger.info("POST 2ndfactor totp: Initiate TOTP validation for user %s", userid);
if (!userid) {
logger.error("POST 2ndfactor totp: No user id in the session");
res.status(403);
res.send();
return;
}
const token = req.body.token;
const totpValidator = req.app.get("totp validator");
const userDataStore = req.app.get("user data store");
logger.debug("POST 2ndfactor totp: Fetching secret for user %s", userid);
userDataStore.get_totp_secret(userid)
.then(function (doc: TOTPSecretDocument) {
logger.debug("POST 2ndfactor totp: TOTP secret is %s", JSON.stringify(doc));
return totpValidator.validate(token, doc.secret.base32);
})
.then(function () {
logger.debug("POST 2ndfactor totp: TOTP validation succeeded");
objectPath.set(req, "session.auth_session.second_factor", true);
res.status(204);
res.send();
})
.catch(exceptions.InvalidTOTPError, function (err: Error) {
logger.error("POST 2ndfactor totp: Invalid TOTP token %s", err.message);
res.status(401);
res.send("Invalid TOTP token");
})
.catch(function (err: Error) {
console.log(err.stack);
logger.error("POST 2ndfactor totp: Internal error %s", err.message);
res.status(500);
res.send("Internal error");
});
};

View File

@ -1,86 +0,0 @@
import objectPath = require("object-path");
import BluebirdPromise = require("bluebird");
import express = require("express");
import exceptions = require("../Exceptions");
import { Identity } from "../../types/Identity";
import { IdentityValidable } from "../IdentityValidator";
const CHALLENGE = "totp-register";
const TEMPLATE_NAME = "totp-register";
class TOTPRegistrationHandler implements IdentityValidable {
challenge(): string {
return CHALLENGE;
}
templateName(): string {
return TEMPLATE_NAME;
}
preValidation(req: express.Request): BluebirdPromise<Identity> {
const first_factor_passed = objectPath.get(req, "session.auth_session.first_factor");
if (!first_factor_passed) {
return BluebirdPromise.reject("Authentication required before registering TOTP secret key");
}
const userid = objectPath.get<express.Request, string>(req, "session.auth_session.userid");
const email = objectPath.get<express.Request, string>(req, "session.auth_session.email");
if (!(userid && email)) {
return BluebirdPromise.reject("User ID or email is missing");
}
const identity = {
email: email,
userid: userid
};
return BluebirdPromise.resolve(identity);
}
mailSubject(): string {
return "Register your TOTP secret key";
}
}
// Generate a secret and send it to the user
function post(req: express.Request, res: express.Response) {
const logger = req.app.get("logger");
const userid = objectPath.get(req, "session.auth_session.identity_check.userid");
const challenge = objectPath.get(req, "session.auth_session.identity_check.challenge");
if (challenge != CHALLENGE || !userid) {
res.status(403);
res.send();
return;
}
const user_data_store = req.app.get("user data store");
const totpGenerator = req.app.get("totp generator");
const secret = totpGenerator.generate();
logger.debug("POST new-totp-secret: save the TOTP secret in DB");
user_data_store.set_totp_secret(userid, secret)
.then(function () {
const doc = {
otpauth_url: secret.otpauth_url,
base32: secret.base32,
ascii: secret.ascii
};
objectPath.set(req, "session", undefined);
res.status(200);
res.json(doc);
})
.catch(function (err: Error) {
logger.error("POST new-totp-secret: Internal error %s", err);
res.status(500);
res.send();
});
}
export = {
icheck_interface: new TOTPRegistrationHandler(),
post: post,
};

View File

@ -1,84 +0,0 @@
import u2f_register_handler = require("./U2FRegistration");
import objectPath = require("object-path");
import u2f_common = require("./u2f_common");
import BluebirdPromise = require("bluebird");
import express = require("express");
import authdog = require("../../types/authdog");
import UserDataStore, { U2FMetaDocument } from "../UserDataStore";
function retrieve_u2f_meta(req: express.Request, userDataStore: UserDataStore) {
const userid = req.session.auth_session.userid;
const appid = u2f_common.extract_app_id(req);
return userDataStore.get_u2f_meta(userid, appid);
}
function sign_request(req: express.Request, res: express.Response) {
const logger = req.app.get("logger");
const userDataStore = req.app.get("user data store");
retrieve_u2f_meta(req, userDataStore)
.then(function (doc: U2FMetaDocument) {
if (!doc) {
u2f_common.reply_with_missing_registration(res);
return;
}
const u2f = req.app.get("u2f");
const meta = doc.meta;
const appid = u2f_common.extract_app_id(req);
logger.info("U2F sign_request: Start authentication to app %s", appid);
return u2f.startAuthentication(appid, [meta]);
})
.then(function (authRequest: authdog.AuthenticationRequest) {
logger.info("U2F sign_request: Store authentication request and reply");
req.session.auth_session.sign_request = authRequest;
res.status(200);
res.json(authRequest);
})
.catch(function (err: Error) {
logger.info("U2F sign_request: %s", err);
res.status(500);
res.send();
});
}
function sign(req: express.Request, res: express.Response) {
if (!objectPath.has(req, "session.auth_session.sign_request")) {
u2f_common.reply_with_unauthorized(res);
return;
}
const logger = req.app.get("logger");
const userDataStore = req.app.get("user data store");
retrieve_u2f_meta(req, userDataStore)
.then(function (doc: U2FMetaDocument) {
const appid = u2f_common.extract_app_id(req);
const u2f = req.app.get("u2f");
const authRequest = req.session.auth_session.sign_request;
const meta = doc.meta;
logger.info("U2F sign: Finish authentication");
return u2f.finishAuthentication(authRequest, req.body, [meta]);
})
.then(function (authenticationStatus: authdog.Authentication) {
logger.info("U2F sign: Authentication successful");
req.session.auth_session.second_factor = true;
res.status(204);
res.send();
})
.catch(function (err: Error) {
logger.error("U2F sign: %s", err);
res.status(500);
res.send();
});
}
export = {
sign_request: sign_request,
sign: sign
};

View File

@ -1,51 +0,0 @@
import objectPath = require("object-path");
import BluebirdPromise = require("bluebird");
import express = require("express");
import { IdentityValidable } from "../IdentityValidator";
import { Identity } from "../../types/Identity";
const CHALLENGE = "u2f-register";
const TEMPLATE_NAME = "u2f-register";
const MAIL_SUBJECT = "Register your U2F device";
class U2FRegistrationHandler implements IdentityValidable {
challenge(): string {
return CHALLENGE;
}
templateName(): string {
return TEMPLATE_NAME;
}
preValidation(req: express.Request): BluebirdPromise<Identity> {
const first_factor_passed = objectPath.get(req, "session.auth_session.first_factor");
if (!first_factor_passed) {
return BluebirdPromise.reject("Authentication required before issuing a u2f registration request");
}
const userid = objectPath.get<express.Request, string>(req, "session.auth_session.userid");
const email = objectPath.get<express.Request, string>(req, "session.auth_session.email");
if (!(userid && email)) {
return BluebirdPromise.reject("User ID or email is missing");
}
const identity = {
email: email,
userid: userid
};
return BluebirdPromise.resolve(identity);
}
mailSubject(): string {
return MAIL_SUBJECT;
}
}
export = {
icheck_interface: new U2FRegistrationHandler(),
};

View File

@ -1,89 +0,0 @@
import u2f_register_handler = require("./U2FRegistration");
import objectPath = require("object-path");
import u2f_common = require("./u2f_common");
import BluebirdPromise = require("bluebird");
import express = require("express");
import authdog = require("../../types/authdog");
function register_request(req: express.Request, res: express.Response) {
const logger = req.app.get("logger");
const challenge = objectPath.get(req, "session.auth_session.identity_check.challenge");
if (challenge != "u2f-register") {
res.status(403);
res.send();
return;
}
const u2f = req.app.get("u2f");
const appid = u2f_common.extract_app_id(req);
logger.debug("U2F register_request: headers=%s", JSON.stringify(req.headers));
logger.info("U2F register_request: Starting registration of app %s", appid);
u2f.startRegistration(appid, [])
.then(function (registrationRequest: authdog.AuthenticationRequest) {
logger.info("U2F register_request: Sending back registration request");
req.session.auth_session.register_request = registrationRequest;
res.status(200);
res.json(registrationRequest);
})
.catch(function (err: Error) {
logger.error("U2F register_request: %s", err);
res.status(500);
res.send("Unable to start registration request");
});
}
function register(req: express.Request, res: express.Response) {
const registrationRequest = objectPath.get(req, "session.auth_session.register_request");
const challenge = objectPath.get(req, "session.auth_session.identity_check.challenge");
if (!registrationRequest) {
res.status(403);
res.send();
return;
}
if (!(registrationRequest && challenge == "u2f-register")) {
res.status(403);
res.send();
return;
}
const user_data_storage = req.app.get("user data store");
const u2f = req.app.get("u2f");
const userid = req.session.auth_session.userid;
const appid = u2f_common.extract_app_id(req);
const 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: authdog.Registration) {
logger.info("U2F register: Store registration and reply");
const meta = {
keyHandle: registrationStatus.keyHandle,
publicKey: registrationStatus.publicKey,
certificate: registrationStatus.certificate
};
return user_data_storage.set_u2f_meta(userid, appid, meta);
})
.then(function () {
objectPath.set(req, "session.auth_session.identity_check", undefined);
res.status(204);
res.send();
})
.catch(function (err: Error) {
logger.error("U2F register: %s", err);
res.status(500);
res.send("Unable to register");
});
}
export = {
register_request: register_request,
register: register
};

View File

@ -1,19 +0,0 @@
import U2FRegistrationProcess = require("./U2FRegistrationProcess");
import U2FAuthenticationProcess = require("./U2FAuthenticationProcess");
import express = require("express");
interface U2FRoutes {
register_request: express.RequestHandler;
register: express.RequestHandler;
sign_request: express.RequestHandler;
sign: express.RequestHandler;
}
export = {
register_request: U2FRegistrationProcess.register_request,
register: U2FRegistrationProcess.register,
sign_request: U2FAuthenticationProcess.sign_request,
sign: U2FAuthenticationProcess.sign,
} as U2FRoutes;

View File

@ -1,39 +0,0 @@
import util = require("util");
import express = require("express");
function extract_app_id(req: express.Request) {
return util.format("https://%s", req.headers.host);
}
function extract_original_url(req: express.Request) {
return util.format("https://%s%s", req.headers.host, req.headers["x-original-uri"]);
}
function extract_referrer(req: express.Request) {
return req.headers.referrer;
}
function reply_with_internal_error(res: express.Response, msg: string) {
res.status(500);
res.send(msg);
}
function reply_with_missing_registration(res: express.Response) {
res.status(401);
res.send("Please register before authenticate");
}
function reply_with_unauthorized(res: express.Response) {
res.status(401);
res.send();
}
export = {
extract_app_id: extract_app_id,
extract_original_url: extract_original_url,
extract_referrer: extract_referrer,
reply_with_internal_error: reply_with_internal_error,
reply_with_missing_registration: reply_with_missing_registration,
reply_with_unauthorized: reply_with_unauthorized
};

View File

@ -1,126 +0,0 @@
@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; }
.btn:hover { color: #333333; text-decoration: none; background-color: #e6e6e6; background-position: 0 -15px; -webkit-transition: background-position 0.1s linear; -moz-transition: background-position 0.1s linear; -ms-transition: background-position 0.1s linear; -o-transition: background-position 0.1s linear; transition: background-position 0.1s linear; }
.btn-primary, .btn-primary:hover { text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); color: #ffffff; }
.btn-primary.active { color: rgba(255, 255, 255, 0.75); }
.btn-primary { background-color: #4a77d4; background-image: -moz-linear-gradient(top, #6eb6de, #4a77d4); background-image: -ms-linear-gradient(top, #6eb6de, #4a77d4); background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#6eb6de), to(#4a77d4)); background-image: -webkit-linear-gradient(top, #6eb6de, #4a77d4); background-image: -o-linear-gradient(top, #6eb6de, #4a77d4); background-image: linear-gradient(top, #6eb6de, #4a77d4); background-repeat: repeat-x; filter: progid:dximagetransform.microsoft.gradient(startColorstr=#6eb6de, endColorstr=#4a77d4, GradientType=0); border: 1px solid #3762bc; text-shadow: 1px 1px 1px rgba(0,0,0,0.4); box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.5); }
.btn-primary:hover, .btn-primary:active, .btn-primary.active, .btn-primary.disabled, .btn-primary[disabled] { filter: none; background-color: #4a77d4; }
.btn-block { width: 100%; display:block; }
* { -webkit-box-sizing:border-box; -moz-box-sizing:border-box; -ms-box-sizing:border-box; -o-box-sizing:border-box; box-sizing:border-box; }
html { width: 100%; height:100%; overflow:hidden; }
body {
width: 100%;
height:100%;
font-family: 'Open Sans', sans-serif;
background: #092756;
background: -moz-radial-gradient(0% 100%, ellipse cover, rgba(104,128,138,.4) 10%,rgba(138,114,76,0) 40%),-moz-linear-gradient(top, rgba(57,173,219,.25) 0%, rgba(42,60,87,.4) 100%), -moz-linear-gradient(-45deg, #670d10 0%, #092756 100%);
background: -webkit-radial-gradient(0% 100%, ellipse cover, rgba(104,128,138,.4) 10%,rgba(138,114,76,0) 40%), -webkit-linear-gradient(top, rgba(57,173,219,.25) 0%,rgba(42,60,87,.4) 100%), -webkit-linear-gradient(-45deg, #670d10 0%,#092756 100%);
background: -o-radial-gradient(0% 100%, ellipse cover, rgba(104,128,138,.4) 10%,rgba(138,114,76,0) 40%), -o-linear-gradient(top, rgba(57,173,219,.25) 0%,rgba(42,60,87,.4) 100%), -o-linear-gradient(-45deg, #670d10 0%,#092756 100%);
background: -ms-radial-gradient(0% 100%, ellipse cover, rgba(104,128,138,.4) 10%,rgba(138,114,76,0) 40%), -ms-linear-gradient(top, rgba(57,173,219,.25) 0%,rgba(42,60,87,.4) 100%), -ms-linear-gradient(-45deg, #670d10 0%,#092756 100%);
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%;
left: 50%;
margin: -150px 0 0 -150px;
width:300px;
height:300px;
}
.totp {
position: absolute;
top: 50%;
left: 50%;
margin: -150px 0 0 -150px;
width:400px;
height:300px;
}
h1 { color: #fff; text-shadow: 0 0 10px rgba(0,0,0,0.3); letter-spacing:1px; text-align:center; }
h2 { color: #fff; text-shadow: 0 0 10px rgba(0,0,0,0.3); letter-spacing:1px; text-align:center; font-size: 1em; }
p { color: #fff; text-shadow: 0 0 10px rgba(0,0,0,0.3); letter-spacing:1px; text-align:center; }
a { color: #fff; text-align: center; }
#qrcode img {
margin: auto;
text-align: center;
padding: 10px;
background: white;
}
#secret { font-size: 0.7em; }
input {
width: 100%;
margin-bottom: 10px;
background: rgba(0,0,0,0.3);
border: none;
outline: none;
padding: 10px;
font-size: 13px;
color: #fff;
text-shadow: 1px 1px 1px rgba(0,0,0,0.3);
border: 1px solid rgba(0,0,0,0.3);
border-radius: 4px;
box-shadow: inset 0 -5px 45px rgba(100,100,100,0.2), 0 1px 1px rgba(255,255,255,0.2);
-webkit-transition: box-shadow .5s ease;
-moz-transition: box-shadow .5s ease;
-o-transition: box-shadow .5s ease;
-ms-transition: box-shadow .5s ease;
transition: box-shadow .5s ease;
}
input:focus { box-shadow: inset 0 -5px 45px rgba(100,100,100,0.4), 0 1px 1px rgba(255,255,255,0.2); }
#information {
border: 1px solid black;
padding: 10px 20px;
margin-top: 25px;
font-size: 0.8em;
border-radius: 4px;
}
#information.failure {
background-color: rgb(255, 124, 124);
}
#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;
}
button {
margin-top: 5px;
}

File diff suppressed because one or more lines are too long

View File

@ -1,286 +0,0 @@
(function() {
params={};
location.search.replace(/[?&]+([^=&]+)=([^&]*)/gi,function(s,k,v){params[k]=v});
function get_redirect_param() {
if('redirect' in params)
return params['redirect'];
return;
}
function setupEnterKeypressListener(filter, fn) {
$(filter).on('keydown', 'input', function (e) {
var key = e.which;
switch (key) {
case 13: // enter key code
fn();
break;
default:
break;
}
});
}
function onLoginButtonClicked() {
var username = $('#username').val();
var password = $('#password').val();
validateFirstFactor(username, password, function(err) {
if(err) {
onFirstFactorFailure(err.responseText);
return;
}
onFirstFactorSuccess();
});
}
function onResetPasswordButtonClicked() {
var r = '/reset-password-form';
window.location.replace(r);
}
function onTotpSignButtonClicked() {
var token = $('#totp-token').val();
validateSecondFactorTotp(token, function(err) {
if(err) {
onSecondFactorTotpFailure(err.responseText);
return;
}
onSecondFactorTotpSuccess();
});
}
function onTotpRegisterButtonClicked() {
$.ajax({
type: 'POST',
url: '/totp-register',
data: JSON.stringify({
redirect: get_redirect_param()
}),
contentType: 'application/json',
dataType: 'json',
})
.done(function(data) {
$.notify('An email has been sent to your email address', 'info');
})
.fail(function(xhr, status) {
$.notify('Unable to send you an email', 'error');
});
}
function onU2fSignButtonClicked() {
startU2fAuthentication(function(err) {
if(err) {
onU2fAuthenticationFailure();
return;
}
onU2fAuthenticationSuccess();
}, 120);
}
function onU2fRegistrationButtonClicked() {
askForU2fRegistration(function(err) {
if(err) {
$.notify('Unable to send you an email', 'error');
return;
}
$.notify('An email has been sent to your email address', 'info');
});
}
function askForU2fRegistration(fn) {
$.ajax({
type: 'POST',
url: '/u2f-register',
data: JSON.stringify({
redirect: get_redirect_param()
}),
contentType: 'application/json',
dataType: 'json',
})
.done(function(data) {
fn(undefined, data);
})
.fail(function(xhr, status) {
fn(status);
});
}
function finishU2fAuthentication(url, responseData, fn) {
$.ajax({
type: 'POST',
url: url,
data: JSON.stringify(responseData),
contentType: 'application/json',
dataType: 'json',
})
.done(function(data) {
fn(undefined, data);
})
.fail(function(xhr, status) {
$.notify('Error when finish U2F transaction', 'error');
});
}
function startU2fAuthentication(fn, timeout) {
$.get('/2ndfactor/u2f/sign_request', {}, null, 'json')
.done(function(signResponse) {
var registeredKeys = signResponse.registeredKeys;
$.notify('Please touch the token', 'info');
u2f.sign(
signResponse.appId,
signResponse.challenge,
signResponse.registeredKeys,
function (response) {
if (response.errorCode) {
fn(response);
} else {
finishU2fAuthentication('/2ndfactor/u2f/sign', response, fn);
}
},
timeout
);
})
.fail(function(xhr, status) {
fn(status);
});
}
function validateSecondFactorTotp(token, fn) {
$.post('/2ndfactor/totp', {
token: token,
})
.done(function() {
fn(undefined);
})
.fail(function(err) {
fn(err);
});
}
function validateFirstFactor(username, password, fn) {
$.post('/1stfactor', {
username: username,
password: password,
})
.done(function() {
fn(undefined);
})
.fail(function(err) {
fn(err);
});
}
function redirect() {
var redirect_uri = '/';
if('redirect' in params) {
redirect_uri = params['redirect'];
}
window.location.replace(redirect_uri);
}
function onFirstFactorSuccess() {
$('#username').val('');
$('#password').val('');
enterSecondFactor();
}
function onFirstFactorFailure(err) {
$('#password').val('');
$('#token').val('');
$.notify('Error during authentication: ' + err, 'error');
}
function onAuthenticationSuccess() {
$.notify('Authentication succeeded. You are redirected.', 'success');
redirect();
}
function onSecondFactorTotpSuccess() {
onAuthenticationSuccess();
}
function onSecondFactorTotpFailure(err) {
$.notify('Error while validating TOTP token. Cause: ' + err, 'error');
}
function onU2fAuthenticationSuccess() {
onAuthenticationSuccess();
}
function onU2fAuthenticationFailure(err) {
$.notify('Problem with U2F authentication. Did you register before authenticating?', 'warn');
}
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);
}
function cleanupFirstFactorLoginButton() {
$('#first-factor #login-button').off('click');
}
function setupTotpSignButton() {
$('#second-factor #totp-sign-button').on('click', onTotpSignButtonClicked);
setupEnterKeypressListener('#totp', onTotpSignButtonClicked);
}
function setupTotpRegisterButton() {
$('#second-factor #totp-register-button').on('click', onTotpRegisterButtonClicked);
}
function setupU2fSignButton() {
$('#second-factor #u2f-sign-button').on('click', onU2fSignButtonClicked);
setupEnterKeypressListener('#u2f', onU2fSignButtonClicked);
}
function setupU2fRegistrationButton() {
$('#second-factor #u2f-register-button').on('click', onU2fRegistrationButtonClicked);
}
function setupResetPasswordButton() {
$('#first-factor #reset-password-button').on('click', onResetPasswordButtonClicked);
}
function enterFirstFactor() {
showFirstFactorLayout();
hideSecondFactorLayout();
setupFirstFactorLoginButton();
setupResetPasswordButton();
}
function enterSecondFactor() {
hideFirstFactorLayout();
showSecondFactorLayout();
cleanupFirstFactorLoginButton();
setupTotpSignButton();
setupTotpRegisterButton();
setupU2fSignButton();
setupU2fRegistrationButton();
}
$(document).ready(function() {
enterFirstFactor();
});
})();

File diff suppressed because one or more lines are too long

View File

@ -1,47 +0,0 @@
(function() {
function setupEnterKeypressListener(filter, fn) {
$(filter).on('keydown', 'input', function (e) {
var key = e.which;
switch (key) {
case 13: // enter key code
fn();
break;
default:
break;
}
});
}
function onResetPasswordButtonClicked() {
var username = $('#username').val();
if(!username) {
$.notify('You must provide your username to reset your password.', 'warn');
return;
}
$.post('/reset-password', {
userid: username,
})
.done(function() {
$.notify('An email has been sent. Click on the link to change your password', 'success');
setTimeout(function() {
window.location.replace('/login');
}, 1000);
})
.fail(function() {
$.notify('Are you sure this is your username?', 'warn');
});
}
function setupResetPasswordButton() {
$('#reset-password-button').on('click', onResetPasswordButtonClicked);
}
$(document).ready(function() {
setupResetPasswordButton();
setupEnterKeypressListener('#reset-password-form', onResetPasswordButtonClicked);
});
})();

View File

@ -1,51 +0,0 @@
(function() {
function setupEnterKeypressListener(filter, fn) {
$(filter).on('keydown', 'input', function (e) {
var key = e.which;
switch (key) {
case 13: // enter key code
fn();
break;
default:
break;
}
});
}
function onResetPasswordButtonClicked() {
var password1 = $('#password1').val();
var password2 = $('#password2').val();
if(!password1 || !password2) {
$.notify('You must enter your new password twice.', 'warn');
return;
}
if(password1 != password2) {
$.notify('The passwords are different', 'warn');
return;
}
$.post('/new-password', {
password: password1,
})
.done(function() {
$.notify('Your password has been changed. Please login again', 'success');
window.location.replace('/login');
})
.fail(function() {
$.notify('An error occurred during password change.', 'warn');
});
}
function setupResetPasswordButton() {
$('#reset-password-button').on('click', onResetPasswordButtonClicked);
}
$(document).ready(function() {
setupResetPasswordButton();
setupEnterKeypressListener('#reset-password-form', onResetPasswordButtonClicked);
});
})();

View File

@ -1,42 +0,0 @@
(function() {
params={};
location.search.replace(/[?&]+([^=&]+)=([^&]*)/gi,function(s,k,v){params[k]=v});
function generateSecret(fn) {
$.ajax({
type: 'POST',
url: '/new-totp-secret',
contentType: 'application/json',
dataType: 'json',
})
.done(function(data) {
fn(undefined, data);
})
.fail(function(xhr, status) {
$.notify('Error when generating TOTP secret');
});
}
function onSecretGenerated(err, secret) {
console.log('secret generated successfully', secret);
console.log('OTP Auth URL=', secret.otpauth_url);
new QRCode(document.getElementById("qrcode"), secret.otpauth_url);
$("#secret").text(secret.base32);
}
function redirect() {
var redirect_uri = '/login';
if('redirect' in params) {
redirect_uri = params['redirect'];
}
window.location.replace(redirect_uri);
}
$(document).ready(function() {
generateSecret(onSecretGenerated);
$('#login-button').on('click', function() {
redirect();
});
});
})();

View File

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

View File

@ -1,67 +0,0 @@
(function() {
params={};
location.search.replace(/[?&]+([^=&]+)=([^&]*)/gi,function(s,k,v){params[k]=v});
function finishRegister(url, responseData, fn) {
$.ajax({
type: 'POST',
url: url,
data: JSON.stringify(responseData),
contentType: 'application/json',
dataType: 'json',
})
.done(function(data) {
fn(undefined, data);
})
.fail(function(xhr, status) {
$.notify('Error when finish U2F transaction' + status);
});
}
function startRegister(fn, timeout) {
$.get('/2ndfactor/u2f/register_request', {}, null, 'json')
.done(function(startRegisterResponse) {
u2f.register(
startRegisterResponse.appId,
startRegisterResponse.registerRequests,
startRegisterResponse.registeredKeys,
function (response) {
if (response.errorCode) {
fn(response.errorCode);
} else {
finishRegister('/2ndfactor/u2f/register', response, fn);
}
},
timeout
);
});
}
function redirect() {
var redirect_uri = '/login';
if('redirect' in params) {
redirect_uri = params['redirect'];
}
window.location.replace(redirect_uri);
}
function onRegisterSuccess() {
redirect();
}
function onRegisterFailure(err) {
$.notify('Problem authenticating with U2F.', 'error');
}
$(document).ready(function() {
startRegister(function(err) {
if(err) {
onRegisterFailure(err);
return;
}
onRegisterSuccess();
}, 240);
});
})();

296
src/server/endpoints.ts Normal file
View File

@ -0,0 +1,296 @@
/**
* @apiDefine UserSession
* @apiHeader {String} Cookie Cookie containing "connect.sid", the user
* session token.
*/
/**
* @apiDefine InternalError
* @apiError (Error 500) {String} error Internal error message.
*/
/**
* @apiDefine IdentityValidationStart
*
* @apiSuccess (Success 204) status Identity validation has been initiated.
* @apiError (Error 403) AccessDenied Access is denied.
* @apiError (Error 400) InvalidIdentity User identity is invalid.
* @apiError (Error 500) {String} error Internal error message.
*
* @apiDescription This request issue an identity validation token for the user
* bound to the session. It sends a challenge to the email address set in the user
* LDAP entry. The user must visit the sent URL to complete the validation and
* continue the registration process.
*/
/**
* @apiDefine IdentityValidationFinish
* @apiParam {String} identity_token The one-time identity validation token provided in the email.
* @apiSuccess (Success 200) {String} content The content of the page.
* @apiError (Error 403) AccessDenied Access is denied.
* @apiError (Error 500) {String} error Internal error message.
*/
/**
* @api {post} /api/secondfactor/u2f/register Complete U2F registration
* @apiName FinishU2FRegistration
* @apiGroup U2F
* @apiVersion 1.0.0
* @apiUse UserSession
* @apiUse InternalError
*
* @apiSuccess (Success 302) Redirect to the URL that has been stored during last call to /verify.
*
* @apiDescription Complete U2F registration request.
*/
export const SECOND_FACTOR_U2F_REGISTER_POST = "/api/u2f/register";
/**
* @api {get} /api/u2f/register_request Start U2F registration
* @apiName StartU2FRegistration
* @apiGroup U2F
* @apiVersion 1.0.0
* @apiUse UserSession
* @apiUse InternalError
*
* @apiSuccess (Success 200) authentication_request The U2F registration request.
* @apiError (Error 403) {none} error Unexpected identity validation challenge.
*
* @apiDescription Initiate a U2F device registration request.
*/
export const SECOND_FACTOR_U2F_REGISTER_REQUEST_GET = "/api/u2f/register_request";
/**
* @api {post} /api/u2f/sign Complete U2F authentication
* @apiName CompleteU2FAuthentication
* @apiGroup U2F
* @apiVersion 1.0.0
* @apiUse UserSession
* @apiUse InternalError
*
* @apiSuccess (Success 302) Redirect to the URL that has been stored during last call to /verify.
* @apiError (Error 403) {none} error No authentication request has been provided.
*
* @apiDescription Complete authentication request of the U2F device.
*/
export const SECOND_FACTOR_U2F_SIGN_POST = "/api/u2f/sign";
/**
* @api {get} /api/u2f/sign_request Start U2F authentication
* @apiName StartU2FAuthentication
* @apiGroup U2F
* @apiVersion 1.0.0
* @apiUse UserSession
* @apiUse InternalError
*
* @apiSuccess (Success 200) authentication_request The U2F authentication request.
* @apiError (Error 401) {none} error There is no key registered for user in session.
*
* @apiDescription Initiate an authentication request using a U2F device.
*/
export const SECOND_FACTOR_U2F_SIGN_REQUEST_GET = "/api/u2f/sign_request";
/**
* @api {post} /api/totp Complete TOTP authentication
* @apiName ValidateTOTPSecondFactor
* @apiGroup TOTP
* @apiVersion 1.0.0
* @apiUse UserSession
* @apiUse InternalError
*
* @apiParam {String} token TOTP token.
*
* @apiSuccess (Success 302) Redirect to the URL that has been stored during last call to /verify.
* @apiError (Error 401) {none} error TOTP token is invalid.
*
* @apiDescription Verify TOTP token. The user is authenticated upon success.
*/
export const SECOND_FACTOR_TOTP_POST = "/api/totp";
/**
* @api {get} /secondfactor/u2f/identity/start Start U2F registration identity validation
* @apiName RequestU2FRegistration
* @apiGroup U2F
* @apiVersion 1.0.0
* @apiUse UserSession
* @apiUse IdentityValidationStart
*/
export const SECOND_FACTOR_U2F_IDENTITY_START_GET = "/secondfactor/u2f/identity/start";
/**
* @api {get} /secondfactor/u2f/identity/finish Finish U2F registration identity validation
* @apiName ServeU2FRegistrationPage
* @apiGroup U2F
* @apiVersion 1.0.0
* @apiUse UserSession
* @apiUse IdentityValidationFinish
*
* @apiDescription Serves the U2F registration page that asks the user to
* touch the token of the U2F device.
*/
export const SECOND_FACTOR_U2F_IDENTITY_FINISH_GET = "/secondfactor/u2f/identity/finish";
/**
* @api {get} /secondfactor/totp/identity/start Start TOTP registration identity validation
* @apiName StartTOTPRegistration
* @apiGroup TOTP
* @apiVersion 1.0.0
* @apiUse UserSession
* @apiUse IdentityValidationStart
*
* @apiDescription Initiates the identity validation
*/
export const SECOND_FACTOR_TOTP_IDENTITY_START_GET = "/secondfactor/totp/identity/start";
/**
* @api {get} /secondfactor/totp/identity/finish Finish TOTP registration identity validation
* @apiName FinishTOTPRegistration
* @apiGroup TOTP
* @apiVersion 1.0.0
* @apiUse UserSession
* @apiUse IdentityValidationFinish
*
*
* @apiDescription Serves the TOTP registration page that displays the secret.
* The secret is a QRCode and a base32 secret.
*/
export const SECOND_FACTOR_TOTP_IDENTITY_FINISH_GET = "/secondfactor/totp/identity/finish";
/**
* @api {post} /api/password-reset Set new password
* @apiName SetNewLDAPPassword
* @apiGroup PasswordReset
* @apiVersion 1.0.0
* @apiUse UserSession
*
* @apiParam {String} password New password
*
* @apiDescription Set a new password for the user.
*/
export const RESET_PASSWORD_FORM_POST = "/api/password-reset";
/**
* @api {get} /password-reset/request Request username
* @apiName ServePasswordResetPage
* @apiGroup PasswordReset
* @apiVersion 1.0.0
* @apiUse UserSession
*
* @apiDescription Serve a page that requires the username.
*/
export const RESET_PASSWORD_REQUEST_GET = "/password-reset/request";
/**
* @api {get} /password-reset/identity/start Start password reset request
* @apiName StartPasswordResetRequest
* @apiGroup PasswordReset
* @apiVersion 1.0.0
* @apiUse UserSession
* @apiUse IdentityValidationStart
*
* @apiDescription Start password reset request.
*/
export const RESET_PASSWORD_IDENTITY_START_GET = "/password-reset/identity/start";
/**
* @api {post} /reset-password/request Finish password reset request
* @apiName FinishPasswordResetRequest
* @apiGroup PasswordReset
* @apiVersion 1.0.0
* @apiUse UserSession
* @apiUse IdentityValidationFinish
*
* @apiDescription Start password reset request.
*/
export const RESET_PASSWORD_IDENTITY_FINISH_GET = "/password-reset/identity/finish";
/**
* @api {post} /1stfactor Bind user against LDAP
* @apiName ValidateFirstFactor
* @apiGroup Authentication
* @apiVersion 1.0.0
* @apiUse UserSession
* @apiUse InternalError
*
* @apiParam {String} username User username.
* @apiParam {String} password User password.
*
* @apiSuccess (Success 204) status 1st factor is validated.
* @apiError (Error 401) {none} error 1st factor is not validated.
* @apiError (Error 401) {none} error Access has been restricted after too
* many authentication attempts
*
* @apiDescription Verify credentials against the LDAP.
*/
export const FIRST_FACTOR_POST = "/api/firstfactor";
/**
* @api {get} / First factor page
* @apiName Login
* @apiGroup Authentication
* @apiVersion 1.0.0
*
* @apiSuccess (Success 200) {String} Content The content of the first factor page.
*
* @apiDescription Serves the login page and create a create a cookie for the client.
*/
export const FIRST_FACTOR_GET = "/";
/**
* @api {get} /secondfactor Second factor page
* @apiName SecondFactor
* @apiGroup Authentication
* @apiVersion 1.0.0
*
* @apiSuccess (Success 200) {String} Content The content of second factor page.
*
* @apiDescription Serves the second factor page
*/
export const SECOND_FACTOR_GET = "/secondfactor";
/**
* @api {get} /verify Verify user authentication
* @apiName VerifyAuthentication
* @apiGroup Verification
* @apiVersion 1.0.0
* @apiUse UserSession
*
* @apiSuccess (Success 204) status The user is authenticated.
* @apiError (Error 401) status The user is not authenticated.
*
* @apiDescription Verify that the user is authenticated, i.e., the two
* factors have been validated
*/
export const VERIFY_GET = "/verify";
/**
* @api {get} /logout Serves logout page
* @apiName Logout
* @apiGroup Authentication
* @apiVersion 1.0.0
*
* @apiParam {String} redirect Redirect to this URL when user is deauthenticated.
* @apiSuccess (Success 302) redirect Redirect to the URL.
*
* @apiDescription Log out the user and redirect to the URL.
*/
export const LOGOUT_GET = "/logout";
export const ERROR_401_GET = "/error/401";
export const ERROR_403_GET = "/error/403";
export const ERROR_404_GET = "/error/404";

View File

@ -17,7 +17,7 @@ console.log("Parse configuration file: %s", config_path);
const yaml_config = YAML.load(config_path);
const deps = {
u2f: require("authdog"),
u2f: require("u2f"),
nodemailer: require("nodemailer"),
ldapjs: require("ldapjs"),
session: require("express-session"),

View File

@ -9,7 +9,7 @@ interface DatedDocument {
date: Date;
}
export default class AuthenticationRegulator {
export class AuthenticationRegulator {
private _user_data_store: any;
private _lock_time_in_seconds: number;

View File

@ -0,0 +1,40 @@
import express = require("express");
import U2f = require("u2f");
export interface AuthenticationSession {
userid: string;
first_factor: boolean;
second_factor: boolean;
identity_check?: {
challenge: string;
userid: string;
};
register_request?: U2f.Request;
sign_request?: U2f.Request;
email: string;
groups: string[];
redirect?: string;
}
export function reset(req: express.Request): void {
const authSession: AuthenticationSession = {
first_factor: false,
second_factor: false,
userid: undefined,
email: undefined,
groups: [],
register_request: undefined,
sign_request: undefined,
identity_check: undefined,
redirect: undefined
};
req.session.auth = authSession;
}
export function get(req: express.Request): AuthenticationSession {
if (!req.session.auth)
reset(req);
return req.session.auth;
}

View File

@ -0,0 +1,18 @@
import BluebirdPromise = require("bluebird");
import express = require("express");
import objectPath = require("object-path");
import FirstFactorValidator = require("./FirstFactorValidator");
import AuthenticationSession = require("./AuthenticationSession");
export function validate(req: express.Request): BluebirdPromise<void> {
return FirstFactorValidator.validate(req)
.then(function () {
const authSession = AuthenticationSession.get(req);
if (!authSession.second_factor)
return BluebirdPromise.reject("No second factor variable");
return BluebirdPromise.resolve();
});
}

View File

@ -1,6 +1,6 @@
import * as ObjectPath from "object-path";
import { AppConfiguration, UserConfiguration, NotifierConfiguration, ACLConfiguration, LdapConfiguration } from "./Configuration";
import { AppConfiguration, UserConfiguration, NotifierConfiguration, ACLConfiguration, LdapConfiguration } from "./../../types/Configuration";
function get_optional<T>(config: object, path: string, default_value: T): T {

View File

@ -0,0 +1,26 @@
import express = require("express");
import { Winston } from "winston";
function replyWithError(res: express.Response, code: number, logger: Winston) {
return function (err: Error) {
logger.error("Reply with error %d: %s", code, err);
res.status(code);
res.send();
};
}
export function replyWithError400(res: express.Response, logger: Winston) {
return replyWithError(res, 400, logger);
}
export function replyWithError401(res: express.Response, logger: Winston) {
return replyWithError(res, 401, logger);
}
export function replyWithError403(res: express.Response, logger: Winston) {
return replyWithError(res, 403, logger);
}
export function replyWithError500(res: express.Response, logger: Winston) {
return replyWithError(res, 500, logger);
}

View File

@ -1,9 +1,9 @@
export class LdapSeachError extends Error {
export class LdapSearchError extends Error {
constructor(message?: string) {
super(message);
this.name = "LdapSeachError";
Object.setPrototypeOf(this, LdapSeachError.prototype);
this.name = "LdapSearchError";
Object.setPrototypeOf(this, LdapSearchError.prototype);
}
}
@ -54,3 +54,19 @@ export class DomainAccessDenied extends Error {
Object.setPrototypeOf(this, DomainAccessDenied.prototype);
}
}
export class FirstFactorValidationError extends Error {
constructor(message?: string) {
super(message);
this.name = "FirstFactorValidationError";
Object.setPrototypeOf(this, FirstFactorValidationError.prototype);
}
}
export class SecondFactorValidationError extends Error {
constructor(message?: string) {
super(message);
this.name = "SecondFactorValidationError";
Object.setPrototypeOf(this, FirstFactorValidationError.prototype);
}
}

View File

@ -0,0 +1,14 @@
import BluebirdPromise = require("bluebird");
import express = require("express");
import objectPath = require("object-path");
import Exceptions = require("./Exceptions");
import AuthenticationSession = require("./AuthenticationSession");
export function validate(req: express.Request): BluebirdPromise<void> {
const authSession = AuthenticationSession.get(req);
if (!authSession.userid || !authSession.first_factor)
return BluebirdPromise.reject(new Exceptions.FirstFactorValidationError("First factor has not been validated yet."));
return BluebirdPromise.resolve();
}

View File

@ -0,0 +1,130 @@
import objectPath = require("object-path");
import randomstring = require("randomstring");
import BluebirdPromise = require("bluebird");
import util = require("util");
import Exceptions = require("./Exceptions");
import fs = require("fs");
import ejs = require("ejs");
import UserDataStore from "./UserDataStore";
import { Winston } from "../../types/Dependencies";
import express = require("express");
import ErrorReplies = require("./ErrorReplies");
import ServerVariables = require("./ServerVariables");
import AuthenticationSession = require("./AuthenticationSession");
import Identity = require("../../types/Identity");
import { IdentityValidationRequestContent } from "./UserDataStore";
const filePath = __dirname + "/../resources/email-template.ejs";
const email_template = fs.readFileSync(filePath, "utf8");
// IdentityValidator allows user to go through a identity validation process in two steps:
// - Request an operation to be performed (password reset, registration).
// - Confirm operation with email.
export interface IdentityValidable {
challenge(): string;
preValidationInit(req: express.Request): BluebirdPromise<Identity.Identity>;
postValidationInit(req: express.Request): BluebirdPromise<void>;
preValidationResponse(req: express.Request, res: express.Response): void; // Serves a page after identity check request
postValidationResponse(req: express.Request, res: express.Response): void; // Serves the page if identity validated
mailSubject(): string;
}
function issue_token(userid: string, content: Object, userDataStore: UserDataStore, logger: Winston): BluebirdPromise<string> {
const five_minutes = 4 * 60 * 1000;
const token = randomstring.generate({ length: 64 });
const that = this;
logger.debug("identity_check: issue identity token %s for 5 minutes", token);
return userDataStore.issue_identity_check_token(userid, token, content, five_minutes)
.then(function () {
return BluebirdPromise.resolve(token);
});
}
function consume_token(token: string, userDataStore: UserDataStore, logger: Winston): BluebirdPromise<IdentityValidationRequestContent> {
logger.debug("identity_check: consume token %s", token);
return userDataStore.consume_identity_check_token(token);
}
export function register(app: express.Application, pre_validation_endpoint: string, post_validation_endpoint: string, handler: IdentityValidable) {
app.get(pre_validation_endpoint, get_start_validation(handler, post_validation_endpoint));
app.get(post_validation_endpoint, get_finish_validation(handler));
}
function checkIdentityToken(req: express.Request, identityToken: string): BluebirdPromise<void> {
if (!identityToken)
return BluebirdPromise.reject(new Exceptions.AccessDeniedError("No identity token provided"));
return BluebirdPromise.resolve();
}
export function get_finish_validation(handler: IdentityValidable): express.RequestHandler {
return function (req: express.Request, res: express.Response): BluebirdPromise<void> {
const logger = ServerVariables.getLogger(req.app);
const userDataStore = ServerVariables.getUserDataStore(req.app);
const authSession = AuthenticationSession.get(req);
const identityToken = objectPath.get<express.Request, string>(req, "query.identity_token");
logger.info("GET identity_check: identity token provided is %s", identityToken);
return checkIdentityToken(req, identityToken)
.then(function () {
return handler.postValidationInit(req);
})
.then(function () {
return consume_token(identityToken, userDataStore, logger);
})
.then(function (content: IdentityValidationRequestContent) {
authSession.identity_check = {
challenge: handler.challenge(),
userid: content.userid
};
handler.postValidationResponse(req, res);
return BluebirdPromise.resolve();
})
.catch(Exceptions.FirstFactorValidationError, ErrorReplies.replyWithError401(res, logger))
.catch(Exceptions.AccessDeniedError, ErrorReplies.replyWithError403(res, logger))
.catch(ErrorReplies.replyWithError500(res, logger));
};
}
export function get_start_validation(handler: IdentityValidable, postValidationEndpoint: string): express.RequestHandler {
return function (req: express.Request, res: express.Response): BluebirdPromise<void> {
const logger = ServerVariables.getLogger(req.app);
const notifier = ServerVariables.getNotifier(req.app);
const userDataStore = ServerVariables.getUserDataStore(req.app);
let identity: Identity.Identity;
logger.info("Identity Validation: Start identity validation");
return handler.preValidationInit(req)
.then(function (id: Identity.Identity) {
logger.debug("Identity Validation: retrieved identity is %s", JSON.stringify(id));
identity = id;
const email_address = objectPath.get<Identity.Identity, string>(identity, "email");
const userid = objectPath.get<Identity.Identity, string>(identity, "userid");
if (!(email_address && userid))
return BluebirdPromise.reject(new Exceptions.IdentityError("Missing user id or email address"));
return issue_token(userid, undefined, userDataStore, logger);
})
.then(function (token: string) {
const host = req.get("Host");
const link_url = util.format("https://%s%s?identity_token=%s", host, postValidationEndpoint, token);
logger.info("POST identity_check: notification sent to user %s", identity.userid);
return notifier.notify(identity, handler.mailSubject(), link_url);
})
.then(function () {
handler.preValidationResponse(req, res);
return BluebirdPromise.resolve();
})
.catch(Exceptions.FirstFactorValidationError, ErrorReplies.replyWithError401(res, logger))
.catch(Exceptions.IdentityError, ErrorReplies.replyWithError400(res, logger))
.catch(Exceptions.AccessDeniedError, ErrorReplies.replyWithError403(res, logger))
.catch(ErrorReplies.replyWithError500(res, logger));
};
}

View File

@ -0,0 +1,3 @@
export const PRE_VALIDATION_TEMPLATE = "need-identity-validation";

View File

@ -6,21 +6,21 @@ import Dovehash = require("dovehash");
import ldapjs = require("ldapjs");
import { EventEmitter } from "events";
import { LdapConfiguration } from "./Configuration";
import { Ldapjs } from "../types/Dependencies";
import { ILogger } from "../types/ILogger";
import { LdapConfiguration } from "./../../types/Configuration";
import { Ldapjs } from "../../types/Dependencies";
import { Winston } from "../../types/Dependencies";
interface SearchEntry {
object: any;
}
export class LdapClient {
options: LdapConfiguration;
ldapjs: Ldapjs;
logger: ILogger;
client: ldapjs.ClientAsync;
private options: LdapConfiguration;
private ldapjs: Ldapjs;
private logger: Winston;
private client: ldapjs.ClientAsync;
constructor(options: LdapConfiguration, ldapjs: Ldapjs, logger: ILogger) {
constructor(options: LdapConfiguration, ldapjs: Ldapjs, logger: Winston) {
this.options = options;
this.ldapjs = ldapjs;
this.logger = logger;
@ -60,7 +60,7 @@ export class LdapClient {
this.logger.debug("LDAP: Bind user %s", user_dn);
return this.client.bindAsync(user_dn, password)
.error(function (err) {
.error(function (err: Error) {
throw new exceptions.LdapBindError(err.message);
});
}
@ -75,14 +75,14 @@ export class LdapClient {
doc.push(entry.object);
});
res.on("error", function (err: Error) {
reject(err);
reject(new exceptions.LdapSearchError(err.message));
});
res.on("end", function () {
resolve(doc);
});
})
.catch(function (err) {
reject(err);
.catch(function (err: Error) {
reject(new exceptions.LdapSearchError(err.message));
});
});
}

73
src/server/lib/RestApi.ts Normal file
View File

@ -0,0 +1,73 @@
import express = require("express");
import UserDataStore from "./UserDataStore";
import { Winston } from "../../types/Dependencies";
import FirstFactorGet = require("./routes/firstfactor/get");
import SecondFactorGet = require("./routes/secondfactor/get");
import FirstFactorPost = require("./routes/firstfactor/post");
import LogoutGet = require("./routes/logout/get");
import VerifyGet = require("./routes/verify/get");
import TOTPSignGet = require("./routes/secondfactor/totp/sign/post");
import IdentityCheckMiddleware = require("./IdentityCheckMiddleware");
import TOTPRegistrationIdentityHandler from "./routes/secondfactor/totp/identity/RegistrationHandler";
import U2FRegistrationIdentityHandler from "./routes/secondfactor/u2f/identity/RegistrationHandler";
import ResetPasswordIdentityHandler from "./routes/password-reset/identity/PasswordResetHandler";
import U2FSignPost = require("./routes/secondfactor/u2f/sign/post");
import U2FSignRequestGet = require("./routes/secondfactor/u2f/sign_request/get");
import U2FRegisterPost = require("./routes/secondfactor/u2f/register/post");
import U2FRegisterRequestGet = require("./routes/secondfactor/u2f/register_request/get");
import ResetPasswordFormPost = require("./routes/password-reset/form/post");
import ResetPasswordRequestPost = require("./routes/password-reset/request/get");
import Error401Get = require("./routes/error/401/get");
import Error403Get = require("./routes/error/403/get");
import Error404Get = require("./routes/error/404/get");
import Endpoints = require("../endpoints");
export default class RestApi {
static setup(app: express.Application): void {
app.get(Endpoints.FIRST_FACTOR_GET, FirstFactorGet.default);
app.get(Endpoints.SECOND_FACTOR_GET, SecondFactorGet.default);
app.get(Endpoints.LOGOUT_GET, LogoutGet.default);
IdentityCheckMiddleware.register(app, Endpoints.SECOND_FACTOR_TOTP_IDENTITY_START_GET,
Endpoints.SECOND_FACTOR_TOTP_IDENTITY_FINISH_GET, new TOTPRegistrationIdentityHandler());
IdentityCheckMiddleware.register(app, Endpoints.SECOND_FACTOR_U2F_IDENTITY_START_GET,
Endpoints.SECOND_FACTOR_U2F_IDENTITY_FINISH_GET, new U2FRegistrationIdentityHandler());
IdentityCheckMiddleware.register(app, Endpoints.RESET_PASSWORD_IDENTITY_START_GET,
Endpoints.RESET_PASSWORD_IDENTITY_FINISH_GET, new ResetPasswordIdentityHandler());
app.get(Endpoints.RESET_PASSWORD_REQUEST_GET, ResetPasswordRequestPost.default);
app.post(Endpoints.RESET_PASSWORD_FORM_POST, ResetPasswordFormPost.default);
app.get(Endpoints.VERIFY_GET, VerifyGet.default);
app.post(Endpoints.FIRST_FACTOR_POST, FirstFactorPost.default);
app.post(Endpoints.SECOND_FACTOR_TOTP_POST, TOTPSignGet.default);
app.get(Endpoints.SECOND_FACTOR_U2F_SIGN_REQUEST_GET, U2FSignRequestGet.default);
app.post(Endpoints.SECOND_FACTOR_U2F_SIGN_POST, U2FSignPost.default);
app.get(Endpoints.SECOND_FACTOR_U2F_REGISTER_REQUEST_GET, U2FRegisterRequestGet.default);
app.post(Endpoints.SECOND_FACTOR_U2F_REGISTER_POST, U2FRegisterPost.default);
app.get(Endpoints.ERROR_401_GET, Error401Get.default);
app.get(Endpoints.ERROR_403_GET, Error403Get.default);
app.get(Endpoints.ERROR_404_GET, Error404Get.default);
}
}

70
src/server/lib/Server.ts Normal file
View File

@ -0,0 +1,70 @@
import { AccessController } from "./access_control/AccessController";
import { UserConfiguration } from "./../../types/Configuration";
import { GlobalDependencies } from "../../types/Dependencies";
import { AuthenticationRegulator } from "./AuthenticationRegulator";
import UserDataStore from "./UserDataStore";
import ConfigurationAdapter from "./ConfigurationAdapter";
import {  TOTPValidator } from "./TOTPValidator";
import { TOTPGenerator } from "./TOTPGenerator";
import RestApi from "./RestApi";
import { LdapClient } from "./LdapClient";
import BluebirdPromise = require("bluebird");
import ServerVariables = require("./ServerVariables");
import * as Express from "express";
import * as BodyParser from "body-parser";
import * as Path from "path";
import * as http from "http";
export default class Server {
private httpServer: http.Server;
start(yaml_configuration: UserConfiguration, deps: GlobalDependencies): BluebirdPromise<void> {
const config = ConfigurationAdapter.adapt(yaml_configuration);
const view_directory = Path.resolve(__dirname, "../views");
const public_html_directory = Path.resolve(__dirname, "../public_html");
const app = Express();
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(deps.session({
secret: config.session.secret,
resave: false,
saveUninitialized: true,
cookie: {
secure: false,
maxAge: config.session.expiration,
domain: config.session.domain
},
}));
app.set("views", view_directory);
app.set("view engine", "pug");
// by default the level of logs is info
deps.winston.level = config.logs_level;
console.log("Log level = ", deps.winston.level);
ServerVariables.fill(app, config, deps);
RestApi.setup(app);
return new BluebirdPromise<void>((resolve, reject) => {
this.httpServer = app.listen(config.port, function (err: string) {
console.log("Listening on %d...", config.port);
resolve();
});
});
}
stop() {
this.httpServer.close();
}
}

View File

@ -0,0 +1,103 @@
import winston = require("winston");
import { LdapClient } from "./LdapClient";
import { TOTPValidator } from "./TOTPValidator";
import { TOTPGenerator } from "./TOTPGenerator";
import U2F = require("u2f");
import UserDataStore from "./UserDataStore";
import { INotifier } from "./notifiers/INotifier";
import { AuthenticationRegulator } from "./AuthenticationRegulator";
import Configuration = require("../../types/Configuration");
import { AccessController } from "./access_control/AccessController";
import { NotifierFactory } from "./notifiers/NotifierFactory";
import { GlobalDependencies } from "../../types/Dependencies";
import express = require("express");
export const VARIABLES_KEY = "authelia-variables";
export interface ServerVariables {
logger: typeof winston;
ldap: LdapClient;
totpValidator: TOTPValidator;
totpGenerator: TOTPGenerator;
u2f: typeof U2F;
userDataStore: UserDataStore;
notifier: INotifier;
regulator: AuthenticationRegulator;
config: Configuration.AppConfiguration;
accessController: AccessController;
}
export function fill(app: express.Application, config: Configuration.AppConfiguration, deps: GlobalDependencies) {
const five_minutes = 5 * 60;
const datastore_options = {
directory: config.store_directory,
inMemory: config.store_in_memory
};
const userDataStore = new UserDataStore(datastore_options, deps.nedb);
const regulator = new AuthenticationRegulator(userDataStore, five_minutes);
const notifier = NotifierFactory.build(config.notifier, deps.nodemailer);
const ldap = new LdapClient(config.ldap, deps.ldapjs, deps.winston);
const accessController = new AccessController(config.access_control, deps.winston);
const totpValidator = new TOTPValidator(deps.speakeasy);
const totpGenerator = new TOTPGenerator(deps.speakeasy);
const variables: ServerVariables = {
accessController: accessController,
config: config,
ldap: ldap,
logger: deps.winston,
notifier: notifier,
regulator: regulator,
totpGenerator: totpGenerator,
totpValidator: totpValidator,
u2f: deps.u2f,
userDataStore: userDataStore
};
app.set(VARIABLES_KEY, variables);
}
export function getLogger(app: express.Application): typeof winston {
return (app.get(VARIABLES_KEY) as ServerVariables).logger;
}
export function getUserDataStore(app: express.Application): UserDataStore {
return (app.get(VARIABLES_KEY) as ServerVariables).userDataStore;
}
export function getNotifier(app: express.Application): INotifier {
return (app.get(VARIABLES_KEY) as ServerVariables).notifier;
}
export function getLdapClient(app: express.Application): LdapClient {
return (app.get(VARIABLES_KEY) as ServerVariables).ldap;
}
export function getConfiguration(app: express.Application): Configuration.AppConfiguration {
return (app.get(VARIABLES_KEY) as ServerVariables).config;
}
export function getAuthenticationRegulator(app: express.Application): AuthenticationRegulator {
return (app.get(VARIABLES_KEY) as ServerVariables).regulator;
}
export function getAccessController(app: express.Application): AccessController {
return (app.get(VARIABLES_KEY) as ServerVariables).accessController;
}
export function getTOTPGenerator(app: express.Application): TOTPGenerator {
return (app.get(VARIABLES_KEY) as ServerVariables).totpGenerator;
}
export function getTOTPValidator(app: express.Application): TOTPValidator {
return (app.get(VARIABLES_KEY) as ServerVariables).totpValidator;
}
export function getU2F(app: express.Application): typeof U2F {
return (app.get(VARIABLES_KEY) as ServerVariables).u2f;
}

View File

@ -1,16 +1,16 @@
import * as speakeasy from "speakeasy";
import { Speakeasy } from "../types/Dependencies";
import { Speakeasy } from "../../types/Dependencies";
import BluebirdPromise = require("bluebird");
export default class TOTPGenerator {
export class TOTPGenerator {
private speakeasy: Speakeasy;
constructor(speakeasy: Speakeasy) {
this.speakeasy = speakeasy;
}
generate(options: speakeasy.GenerateOptions): speakeasy.Key {
generate(options?: speakeasy.GenerateOptions): speakeasy.Key {
return this.speakeasy.generateSecret(options);
}
}

View File

@ -1,10 +1,10 @@
import { Speakeasy } from "../types/Dependencies";
import { Speakeasy } from "../../types/Dependencies";
import BluebirdPromise = require("bluebird");
const TOTP_ENCODING = "base32";
export default class TOTPValidator {
export class TOTPValidator {
private speakeasy: Speakeasy;
constructor(speakeasy: Speakeasy) {

Some files were not shown because too many files have changed in this diff Show More