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 # NodeJs modules
node_modules/ node_modules/
# npm debug logs
npm-debug.log*
# Coverage reports # Coverage reports
coverage/ coverage/
@ -24,3 +27,5 @@ notifications/
# Generated by TypeScript compiler # Generated by TypeScript compiler
dist/ dist/
.nyc_output/

View File

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

View File

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

View File

@ -1,10 +1,12 @@
module.exports = function(grunt) { module.exports = function (grunt) {
const buildDir = "dist";
grunt.initConfig({ grunt.initConfig({
run: { run: {
options: {}, options: {},
"build-ts": { "build": {
cmd: "npm", cmd: "npm",
args: ['run', 'build-ts'] args: ['run', 'build']
}, },
"tslint": { "tslint": {
cmd: "npm", cmd: "npm",
@ -17,39 +19,136 @@ module.exports = function(grunt) {
"docker-build": { "docker-build": {
cmd: "docker", cmd: "docker",
args: ['build', '-t', 'clems4ever/authelia', '.'] 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: { copy: {
resources: { resources: {
expand: true, expand: true,
cwd: 'src/resources/', cwd: 'src/server/resources/',
src: '**', src: '**',
dest: 'dist/src/resources/' dest: `${buildDir}/src/server/resources/`
}, },
views: { views: {
expand: true, expand: true,
cwd: 'src/views/', cwd: 'src/server/views/',
src: '**', src: '**',
dest: 'dist/src/views/' dest: `${buildDir}/src/server/views/`
}, },
public_html: { images: {
expand: true, expand: true,
cwd: 'src/public_html/', cwd: 'src/client/img',
src: '**', 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-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.loadNpmTasks('grunt-run');
grunt.loadNpmTasks('grunt-contrib-copy');
grunt.registerTask('default', ['build']); grunt.registerTask('default', ['build']);
grunt.registerTask('res', ['copy:resources', 'copy:views', 'copy:public_html']);
grunt.registerTask('build', ['run:tslint', 'run:build-ts', 'res']); 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('docker-build', ['run:docker-build']); grunt.registerTask('docker-build', ['run:docker-build']);
grunt.registerTask('docker-restart', ['run:docker-restart']);
grunt.registerTask('test', ['run:test']); 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. ./notifications/notification.txt.
Paste the link in your browser and you should be able to reset the password. 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 ### Access Control
With **Authelia**, you can define your own access control rules for restricting 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 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 # 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 # 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({ define({
"title": "Authelia API documentation", "title": "Authelia API documentation",
"name": "authelia", "name": "authelia",
"version": "1.0.11", "version": "2.1.3",
"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",
"sampleUrl": false, "sampleUrl": false,
"defaultVersion": "0.0.0", "defaultVersion": "0.0.0",
"apidoc": "0.3.0", "apidoc": "0.3.0",
"generator": { "generator": {
"name": "apidoc", "name": "apidoc",
"time": "2017-01-29T00:44:17.687Z", "time": "2017-06-11T20:41:36.025Z",
"url": "http://apidocjs.com", "url": "http://apidocjs.com",
"version": "0.17.5" "version": "0.17.6"
} }
}); });

View File

@ -1,15 +1,15 @@
{ {
"title": "Authelia API documentation", "title": "Authelia API documentation",
"name": "authelia", "name": "authelia",
"version": "1.0.11", "version": "2.1.3",
"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",
"sampleUrl": false, "sampleUrl": false,
"defaultVersion": "0.0.0", "defaultVersion": "0.0.0",
"apidoc": "0.3.0", "apidoc": "0.3.0",
"generator": { "generator": {
"name": "apidoc", "name": "apidoc",
"time": "2017-01-29T00:44:17.687Z", "time": "2017-06-11T20:41:36.025Z",
"url": "http://apidocjs.com", "url": "http://apidocjs.com",
"version": "0.17.5" "version": "0.17.6"
} }
} }

View File

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

View File

@ -224,7 +224,7 @@
<div class="tab-content"> <div class="tab-content">
{{#each params.examples}} {{#each params.examples}}
<div class="tab-pane{{#if_eq @index compare=0}} active{{/if_eq}}" id="{{../section}}-examples-{{../id}}-{{@index}}"> <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> </div>
{{/each}} {{/each}}
</div> </div>
@ -274,7 +274,7 @@
{{#each this}} {{#each this}}
<label class="col-md-3 control-label" for="sample-request-param-field-{{field}}">{{field}}</label> <label class="col-md-3 control-label" for="sample-request-param-field-{{field}}">{{field}}</label>
<div class="input-group"> <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 class="input-group-addon">{{{type}}}</div>
</div> </div>
{{/each}} {{/each}}

View File

@ -9,6 +9,8 @@ define([
'./locales/pt_br.js', './locales/pt_br.js',
'./locales/ro.js', './locales/ro.js',
'./locales/ru.js', './locales/ru.js',
'./locales/tr.js',
'./locales/vi.js',
'./locales/zh.js', './locales/zh.js',
'./locales/zh_cn.js' './locales/zh_cn.js'
], function() { ], 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 = {}; var paramType = {};
$root.find(".sample-request-param:checked").each(function(i, element) { $root.find(".sample-request-param:checked").each(function(i, element) {
var group = $(element).data("sample-request-param-group-id"); 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 key = $(element).data("sample-request-param-name");
var value = element.value; var value = element.value;
if ( ! element.optional && element.defaultValue !== '') { if ( ! element.optional && element.defaultValue !== '') {

View File

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

View File

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

View File

@ -30,10 +30,6 @@ http {
ssl_certificate /etc/ssl/server.crt; ssl_certificate /etc/ssl/server.crt;
ssl_certificate_key /etc/ssl/server.key; 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 / { location / {
proxy_set_header X-Original-URI $request_uri; proxy_set_header X-Original-URI $request_uri;
@ -41,18 +37,12 @@ http {
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_pass http://auth/; proxy_pass http://auth/;
}
location /js/ { proxy_intercept_errors on;
proxy_pass http://auth/js/;
}
location /img/ { error_page 401 = /error/401;
proxy_pass http://auth/img/; error_page 403 = /error/403;
} error_page 404 = /error/404;
location /css/ {
proxy_pass http://auth/css/;
} }
} }
@ -61,8 +51,7 @@ http {
root /usr/share/nginx/html; root /usr/share/nginx/html;
server_name secret1.test.local secret2.test.local secret.test.local server_name secret1.test.local secret2.test.local secret.test.local
home.test.local mx1.mail.test.local mx2.mail.test.local home.test.local mx1.mail.test.local mx2.mail.test.local;
localhost;
ssl on; ssl on;
ssl_certificate /etc/ssl/server.crt; ssl_certificate /etc/ssl/server.crt;
@ -70,7 +59,7 @@ http {
error_page 401 = @error401; error_page 401 = @error401;
location @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 { 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", "name": "authelia",
"version": "2.1.9", "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", "main": "src/index.js",
"bin": { "bin": {
"authelia": "src/index.js" "authelia": "src/index.js"
}, },
"scripts": { "scripts": {
"test": "./node_modules/.bin/mocha --compilers ts:ts-node/register --recursive test/unitary", "test": "./node_modules/.bin/mocha --compilers ts:ts-node/register --recursive test/client test/server",
"test-dbg": "./node_modules/.bin/mocha --debug-brk --compilers ts:ts-node/register --recursive test/unitary", "int-test": "./node_modules/.bin/mocha --compilers ts:ts-node/register --recursive test/integration",
"int-test": "./node_modules/.bin/mocha --recursive test/integration", "cover": "NODE_ENV=test nyc npm t",
"coverage": "./node_modules/.bin/istanbul cover _mocha -- -R spec --recursive test", "build": "tsc",
"build-ts": "tsc",
"watch-ts": "tsc -w",
"tslint": "tslint -c tslint.json -p tsconfig.json", "tslint": "tslint -c tslint.json -p tsconfig.json",
"serve": "node dist/src/index.js" "serve": "node dist/server/index.js"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@ -29,7 +27,7 @@
"title": "Authelia API documentation" "title": "Authelia API documentation"
}, },
"dependencies": { "dependencies": {
"authdog": "^0.1.1", "@types/cors": "^2.8.1",
"bluebird": "^3.4.7", "bluebird": "^3.4.7",
"body-parser": "^1.15.2", "body-parser": "^1.15.2",
"dovehash": "0.0.5", "dovehash": "0.0.5",
@ -40,8 +38,10 @@
"nedb": "^1.8.0", "nedb": "^1.8.0",
"nodemailer": "^2.7.0", "nodemailer": "^2.7.0",
"object-path": "^0.11.3", "object-path": "^0.11.3",
"pug": "^2.0.0-rc.2",
"randomstring": "^1.1.5", "randomstring": "^1.1.5",
"speakeasy": "^2.0.0", "speakeasy": "^2.0.0",
"u2f": "^0.1.2",
"winston": "^2.3.1", "winston": "^2.3.1",
"yamljs": "^0.2.8" "yamljs": "^0.2.8"
}, },
@ -52,6 +52,8 @@
"@types/ejs": "^2.3.33", "@types/ejs": "^2.3.33",
"@types/express": "^4.0.35", "@types/express": "^4.0.35",
"@types/express-session": "0.0.32", "@types/express-session": "0.0.32",
"@types/jquery": "^2.0.45",
"@types/jsdom": "^2.0.30",
"@types/ldapjs": "^1.0.0", "@types/ldapjs": "^1.0.0",
"@types/mocha": "^2.2.41", "@types/mocha": "^2.2.41",
"@types/mockdate": "^2.0.0", "@types/mockdate": "^2.0.0",
@ -59,6 +61,7 @@
"@types/nodemailer": "^1.3.32", "@types/nodemailer": "^1.3.32",
"@types/object-path": "^0.9.28", "@types/object-path": "^0.9.28",
"@types/proxyquire": "^1.3.27", "@types/proxyquire": "^1.3.27",
"@types/query-string": "^4.3.1",
"@types/randomstring": "^1.1.5", "@types/randomstring": "^1.1.5",
"@types/request": "0.0.43", "@types/request": "0.0.43",
"@types/sinon": "^2.2.1", "@types/sinon": "^2.2.1",
@ -66,12 +69,25 @@
"@types/tmp": "0.0.33", "@types/tmp": "0.0.33",
"@types/winston": "^2.3.2", "@types/winston": "^2.3.2",
"@types/yamljs": "^0.2.30", "@types/yamljs": "^0.2.30",
"apidoc": "^0.17.6",
"browserify": "^14.3.0",
"grunt": "^1.0.1", "grunt": "^1.0.1",
"grunt-browserify": "^5.0.0",
"grunt-contrib-concat": "^1.0.1",
"grunt-contrib-copy": "^1.0.0", "grunt-contrib-copy": "^1.0.0",
"grunt-contrib-cssmin": "^2.2.0",
"grunt-contrib-watch": "^1.0.0",
"grunt-run": "^0.6.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", "mocha": "^3.2.0",
"mockdate": "^2.0.1", "mockdate": "^2.0.1",
"notifyjs-browser": "^0.4.2",
"nyc": "^10.3.2",
"proxyquire": "^1.8.0", "proxyquire": "^1.8.0",
"query-string": "^4.3.4",
"request": "^2.79.0", "request": "^2.79.0",
"should": "^11.1.1", "should": "^11.1.1",
"sinon": "^1.17.6", "sinon": "^1.17.6",
@ -79,6 +95,31 @@
"tmp": "0.0.31", "tmp": "0.0.31",
"ts-node": "^3.0.4", "ts-node": "^3.0.4",
"tslint": "^5.2.0", "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 yaml_config = YAML.load(config_path);
const deps = { const deps = {
u2f: require("authdog"), u2f: require("u2f"),
nodemailer: require("nodemailer"), nodemailer: require("nodemailer"),
ldapjs: require("ldapjs"), ldapjs: require("ldapjs"),
session: require("express-session"), session: require("express-session"),

View File

@ -9,7 +9,7 @@ interface DatedDocument {
date: Date; date: Date;
} }
export default class AuthenticationRegulator { export class AuthenticationRegulator {
private _user_data_store: any; private _user_data_store: any;
private _lock_time_in_seconds: number; 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 * 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 { 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) { constructor(message?: string) {
super(message); super(message);
this.name = "LdapSeachError"; this.name = "LdapSearchError";
Object.setPrototypeOf(this, LdapSeachError.prototype); Object.setPrototypeOf(this, LdapSearchError.prototype);
} }
} }
@ -54,3 +54,19 @@ export class DomainAccessDenied extends Error {
Object.setPrototypeOf(this, DomainAccessDenied.prototype); 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 ldapjs = require("ldapjs");
import { EventEmitter } from "events"; import { EventEmitter } from "events";
import { LdapConfiguration } from "./Configuration"; import { LdapConfiguration } from "./../../types/Configuration";
import { Ldapjs } from "../types/Dependencies"; import { Ldapjs } from "../../types/Dependencies";
import { ILogger } from "../types/ILogger"; import { Winston } from "../../types/Dependencies";
interface SearchEntry { interface SearchEntry {
object: any; object: any;
} }
export class LdapClient { export class LdapClient {
options: LdapConfiguration; private options: LdapConfiguration;
ldapjs: Ldapjs; private ldapjs: Ldapjs;
logger: ILogger; private logger: Winston;
client: ldapjs.ClientAsync; private client: ldapjs.ClientAsync;
constructor(options: LdapConfiguration, ldapjs: Ldapjs, logger: ILogger) { constructor(options: LdapConfiguration, ldapjs: Ldapjs, logger: Winston) {
this.options = options; this.options = options;
this.ldapjs = ldapjs; this.ldapjs = ldapjs;
this.logger = logger; this.logger = logger;
@ -60,7 +60,7 @@ export class LdapClient {
this.logger.debug("LDAP: Bind user %s", user_dn); this.logger.debug("LDAP: Bind user %s", user_dn);
return this.client.bindAsync(user_dn, password) return this.client.bindAsync(user_dn, password)
.error(function (err) { .error(function (err: Error) {
throw new exceptions.LdapBindError(err.message); throw new exceptions.LdapBindError(err.message);
}); });
} }
@ -75,14 +75,14 @@ export class LdapClient {
doc.push(entry.object); doc.push(entry.object);
}); });
res.on("error", function (err: Error) { res.on("error", function (err: Error) {
reject(err); reject(new exceptions.LdapSearchError(err.message));
}); });
res.on("end", function () { res.on("end", function () {
resolve(doc); resolve(doc);
}); });
}) })
.catch(function (err) { .catch(function (err: Error) {
reject(err); 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 * as speakeasy from "speakeasy";
import { Speakeasy } from "../types/Dependencies"; import { Speakeasy } from "../../types/Dependencies";
import BluebirdPromise = require("bluebird"); import BluebirdPromise = require("bluebird");
export default class TOTPGenerator { export class TOTPGenerator {
private speakeasy: Speakeasy; private speakeasy: Speakeasy;
constructor(speakeasy: Speakeasy) { constructor(speakeasy: Speakeasy) {
this.speakeasy = speakeasy; this.speakeasy = speakeasy;
} }
generate(options: speakeasy.GenerateOptions): speakeasy.Key { generate(options?: speakeasy.GenerateOptions): speakeasy.Key {
return this.speakeasy.generateSecret(options); 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"); import BluebirdPromise = require("bluebird");
const TOTP_ENCODING = "base32"; const TOTP_ENCODING = "base32";
export default class TOTPValidator { export class TOTPValidator {
private speakeasy: Speakeasy; private speakeasy: Speakeasy;
constructor(speakeasy: Speakeasy) { constructor(speakeasy: Speakeasy) {

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