Refactor client to make it responsive and testable
|
@ -2,6 +2,9 @@
|
|||
# NodeJs modules
|
||||
node_modules/
|
||||
|
||||
# npm debug logs
|
||||
npm-debug.log*
|
||||
|
||||
# Coverage reports
|
||||
coverage/
|
||||
|
||||
|
@ -24,3 +27,5 @@ notifications/
|
|||
|
||||
# Generated by TypeScript compiler
|
||||
dist/
|
||||
|
||||
.nyc_output/
|
||||
|
|
|
@ -20,7 +20,7 @@ addons:
|
|||
before_install: npm install -g npm@'>=2.13.5'
|
||||
script:
|
||||
- grunt test
|
||||
- grunt build
|
||||
- grunt dist
|
||||
- grunt docker-build
|
||||
- docker-compose build
|
||||
- docker-compose up -d
|
||||
|
|
|
@ -5,7 +5,7 @@ WORKDIR /usr/src
|
|||
COPY package.json /usr/src/package.json
|
||||
RUN npm install --production
|
||||
|
||||
COPY dist/src /usr/src
|
||||
COPY dist/src/server /usr/src
|
||||
|
||||
ENV PORT=80
|
||||
EXPOSE 80
|
||||
|
|
125
Gruntfile.js
|
@ -1,10 +1,12 @@
|
|||
module.exports = function(grunt) {
|
||||
module.exports = function (grunt) {
|
||||
const buildDir = "dist";
|
||||
|
||||
grunt.initConfig({
|
||||
run: {
|
||||
options: {},
|
||||
"build-ts": {
|
||||
"build": {
|
||||
cmd: "npm",
|
||||
args: ['run', 'build-ts']
|
||||
args: ['run', 'build']
|
||||
},
|
||||
"tslint": {
|
||||
cmd: "npm",
|
||||
|
@ -17,39 +19,136 @@ module.exports = function(grunt) {
|
|||
"docker-build": {
|
||||
cmd: "docker",
|
||||
args: ['build', '-t', 'clems4ever/authelia', '.']
|
||||
},
|
||||
"docker-restart": {
|
||||
cmd: "docker-compose",
|
||||
args: ['-f', 'docker-compose.yml', '-f', 'docker-compose.dev.yml', 'restart', 'auth']
|
||||
},
|
||||
"minify": {
|
||||
cmd: "./node_modules/.bin/uglifyjs",
|
||||
args: [`${buildDir}/src/server/public_html/js/authelia.js`, '-o', `${buildDir}/src/server/public_html/js/authelia.min.js`]
|
||||
},
|
||||
"apidoc": {
|
||||
cmd: "./node_modules/.bin/apidoc",
|
||||
args: ["-i", "src/server", "-o", "doc"]
|
||||
}
|
||||
},
|
||||
copy: {
|
||||
resources: {
|
||||
expand: true,
|
||||
cwd: 'src/resources/',
|
||||
cwd: 'src/server/resources/',
|
||||
src: '**',
|
||||
dest: 'dist/src/resources/'
|
||||
dest: `${buildDir}/src/server/resources/`
|
||||
},
|
||||
views: {
|
||||
expand: true,
|
||||
cwd: 'src/views/',
|
||||
cwd: 'src/server/views/',
|
||||
src: '**',
|
||||
dest: 'dist/src/views/'
|
||||
dest: `${buildDir}/src/server/views/`
|
||||
},
|
||||
public_html: {
|
||||
images: {
|
||||
expand: true,
|
||||
cwd: 'src/public_html/',
|
||||
cwd: 'src/client/img',
|
||||
src: '**',
|
||||
dest: 'dist/src/public_html/'
|
||||
dest: `${buildDir}/src/server/public_html/img/`
|
||||
},
|
||||
thirdparties: {
|
||||
expand: true,
|
||||
cwd: 'src/client/thirdparties',
|
||||
src: '**',
|
||||
dest: `${buildDir}/src/server/public_html/js/`
|
||||
},
|
||||
},
|
||||
browserify: {
|
||||
dist: {
|
||||
src: ['dist/src/client/index.js'],
|
||||
dest: `${buildDir}/src/server/public_html/js/authelia.js`,
|
||||
options: {
|
||||
browserifyOptions: {
|
||||
standalone: 'authelia'
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
views: {
|
||||
files: ['src/server/views/**/*.pug'],
|
||||
tasks: ['copy:views'],
|
||||
options: {
|
||||
interrupt: false,
|
||||
atBegin: true
|
||||
}
|
||||
},
|
||||
resources: {
|
||||
files: ['src/server/resources/*.ejs'],
|
||||
tasks: ['copy:resources'],
|
||||
options: {
|
||||
interrupt: false,
|
||||
atBegin: true
|
||||
}
|
||||
},
|
||||
images: {
|
||||
files: ['src/client/img/**'],
|
||||
tasks: ['copy:images'],
|
||||
options: {
|
||||
interrupt: false,
|
||||
atBegin: true
|
||||
}
|
||||
},
|
||||
css: {
|
||||
files: ['src/client/**/*.css'],
|
||||
tasks: ['concat:css', 'cssmin'],
|
||||
options: {
|
||||
interrupt: true,
|
||||
atBegin: true
|
||||
}
|
||||
},
|
||||
client: {
|
||||
files: ['src/client/**/*.ts', 'test/client/**/*.ts'],
|
||||
tasks: ['build'],
|
||||
options: {
|
||||
interrupt: true,
|
||||
atBegin: true
|
||||
}
|
||||
},
|
||||
server: {
|
||||
files: ['src/server/**/*.ts', 'test/server/**/*.ts'],
|
||||
tasks: ['build', 'run:docker-restart'],
|
||||
options: {
|
||||
interrupt: true,
|
||||
}
|
||||
}
|
||||
},
|
||||
concat: {
|
||||
css: {
|
||||
src: ['src/client/css/*.css'],
|
||||
dest: `${buildDir}/src/server/public_html/css/authelia.css`
|
||||
},
|
||||
},
|
||||
cssmin: {
|
||||
target: {
|
||||
files: {
|
||||
[`${buildDir}/src/server/public_html/css/authelia.min.css`]: [`${buildDir}/src/server/public_html/css/authelia.css`]
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
grunt.loadNpmTasks('grunt-run');
|
||||
grunt.loadNpmTasks('grunt-browserify');
|
||||
grunt.loadNpmTasks('grunt-contrib-concat');
|
||||
grunt.loadNpmTasks('grunt-contrib-copy');
|
||||
grunt.loadNpmTasks('grunt-contrib-cssmin');
|
||||
grunt.loadNpmTasks('grunt-contrib-watch');
|
||||
grunt.loadNpmTasks('grunt-run');
|
||||
|
||||
grunt.registerTask('default', ['build']);
|
||||
|
||||
grunt.registerTask('res', ['copy:resources', 'copy:views', 'copy:public_html']);
|
||||
grunt.registerTask('build-resources', ['copy:resources', 'copy:views', 'copy:images', 'copy:thirdparties', 'concat:css', 'cssmin']);
|
||||
grunt.registerTask('build', ['run:tslint', 'run:build', 'browserify:dist']);
|
||||
grunt.registerTask('dist', ['build', 'build-resources', 'run:minify', 'cssmin']);
|
||||
|
||||
grunt.registerTask('build', ['run:tslint', 'run:build-ts', 'res']);
|
||||
grunt.registerTask('docker-build', ['run:docker-build']);
|
||||
grunt.registerTask('docker-restart', ['run:docker-restart']);
|
||||
|
||||
grunt.registerTask('test', ['run:test']);
|
||||
};
|
||||
|
|
|
@ -117,6 +117,8 @@ email address. For the sake of the example, the email is delivered in the file
|
|||
./notifications/notification.txt.
|
||||
Paste the link in your browser and you should be able to reset the password.
|
||||
|
||||
![reset-password](https://raw.githubusercontent.com/clems4ever/authelia/master/images/reset_password.png)
|
||||
|
||||
### Access Control
|
||||
With **Authelia**, you can define your own access control rules for restricting
|
||||
the access to certain subdomains to your users. Those rules are defined in the
|
||||
|
|
|
@ -76,7 +76,7 @@ session:
|
|||
|
||||
|
||||
# The directory where the DB files will be saved
|
||||
store_directory: /var/lib/auth-server/store
|
||||
store_directory: /var/lib/authelia/store
|
||||
|
||||
|
||||
# Notifications are sent to users when they require a password reset, a u2f
|
||||
|
|
1074
doc/api_data.js
1074
doc/api_data.json
|
@ -1,15 +1,15 @@
|
|||
define({
|
||||
"title": "Authelia API documentation",
|
||||
"name": "authelia",
|
||||
"version": "1.0.11",
|
||||
"description": "2-factor authentication server using LDAP as 1st factor and TOTP or U2F as 2nd factor",
|
||||
"version": "2.1.3",
|
||||
"description": "2FA Single Sign-On server for nginx using LDAP, TOTP and U2F",
|
||||
"sampleUrl": false,
|
||||
"defaultVersion": "0.0.0",
|
||||
"apidoc": "0.3.0",
|
||||
"generator": {
|
||||
"name": "apidoc",
|
||||
"time": "2017-01-29T00:44:17.687Z",
|
||||
"time": "2017-06-11T20:41:36.025Z",
|
||||
"url": "http://apidocjs.com",
|
||||
"version": "0.17.5"
|
||||
"version": "0.17.6"
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
{
|
||||
"title": "Authelia API documentation",
|
||||
"name": "authelia",
|
||||
"version": "1.0.11",
|
||||
"description": "2-factor authentication server using LDAP as 1st factor and TOTP or U2F as 2nd factor",
|
||||
"version": "2.1.3",
|
||||
"description": "2FA Single Sign-On server for nginx using LDAP, TOTP and U2F",
|
||||
"sampleUrl": false,
|
||||
"defaultVersion": "0.0.0",
|
||||
"apidoc": "0.3.0",
|
||||
"generator": {
|
||||
"name": "apidoc",
|
||||
"time": "2017-01-29T00:44:17.687Z",
|
||||
"time": "2017-06-11T20:41:36.025Z",
|
||||
"url": "http://apidocjs.com",
|
||||
"version": "0.17.5"
|
||||
"version": "0.17.6"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -172,6 +172,7 @@ pre {
|
|||
border-radius: 6px;
|
||||
position: relative;
|
||||
margin: 10px 0 20px 0;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
pre.prettyprint {
|
||||
|
|
|
@ -224,7 +224,7 @@
|
|||
<div class="tab-content">
|
||||
{{#each params.examples}}
|
||||
<div class="tab-pane{{#if_eq @index compare=0}} active{{/if_eq}}" id="{{../section}}-examples-{{../id}}-{{@index}}">
|
||||
<pre class="prettyprint language-{{type}}" data-type="{{type}}"><code>{{{reformat content type}}}</code></pre>
|
||||
<pre class="prettyprint language-{{type}}" data-type="{{type}}"><code>{{reformat content type}}</code></pre>
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
|
@ -274,7 +274,7 @@
|
|||
{{#each this}}
|
||||
<label class="col-md-3 control-label" for="sample-request-param-field-{{field}}">{{field}}</label>
|
||||
<div class="input-group">
|
||||
<input id="sample-request-param-field-{{field}}" type="text" placeholder="{{field}}" class="form-control sample-request-param" data-sample-request-param-name="{{field}}" data-sample-request-param-group="sample-request-param-{{@../index}}">
|
||||
<input id="sample-request-param-field-{{field}}" type="text" placeholder="{{field}}" class="form-control sample-request-param" data-sample-request-param-name="{{field}}" data-sample-request-param-group="sample-request-param-{{@../index}}" {{#if optional}}data-sample-request-param-optional="true"{{/if}}>
|
||||
<div class="input-group-addon">{{{type}}}</div>
|
||||
</div>
|
||||
{{/each}}
|
||||
|
|
|
@ -9,6 +9,8 @@ define([
|
|||
'./locales/pt_br.js',
|
||||
'./locales/ro.js',
|
||||
'./locales/ru.js',
|
||||
'./locales/tr.js',
|
||||
'./locales/vi.js',
|
||||
'./locales/zh.js',
|
||||
'./locales/zh_cn.js'
|
||||
], function() {
|
||||
|
|
|
@ -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'
|
||||
}
|
||||
});
|
|
@ -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'
|
||||
}
|
||||
});
|
|
@ -50,7 +50,9 @@ define([
|
|||
var paramType = {};
|
||||
$root.find(".sample-request-param:checked").each(function(i, element) {
|
||||
var group = $(element).data("sample-request-param-group-id");
|
||||
$root.find("[data-sample-request-param-group=\"" + group + "\"]").each(function(i, element) {
|
||||
$root.find("[data-sample-request-param-group=\"" + group + "\"]").not(function(){
|
||||
return $(this).val() == "" && $(this).is("[data-sample-request-param-optional='true']");
|
||||
}).each(function(i, element) {
|
||||
var key = $(element).data("sample-request-param-name");
|
||||
var value = element.value;
|
||||
if ( ! element.optional && element.defaultValue !== '') {
|
||||
|
|
|
@ -4,8 +4,8 @@ services:
|
|||
auth:
|
||||
volumes:
|
||||
- ./test:/usr/src/test
|
||||
- ./src/views:/usr/src/views
|
||||
- ./src/public_html:/usr/src/public_html
|
||||
- ./dist/src/server:/usr/src
|
||||
- ./node_modules:/usr/src/node_modules
|
||||
- ./config.yml:/etc/auth-server/config.yml:ro
|
||||
|
||||
ldap-admin:
|
||||
|
|
|
@ -25,7 +25,7 @@ dn: cn=john,ou=users,dc=example,dc=com
|
|||
cn: john
|
||||
objectclass: inetOrgPerson
|
||||
objectclass: top
|
||||
mail: john.doe@example.com
|
||||
mail: clement.michaud34@gmail.com
|
||||
sn: John Doe
|
||||
userpassword: {SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g=
|
||||
|
||||
|
|
|
@ -30,10 +30,6 @@ http {
|
|||
ssl_certificate /etc/ssl/server.crt;
|
||||
ssl_certificate_key /etc/ssl/server.key;
|
||||
|
||||
error_page 401 = @error401;
|
||||
location @error401 {
|
||||
return 302 https://auth.test.local:8080/login?redirect=$scheme://$http_host$request_uri;
|
||||
}
|
||||
|
||||
location / {
|
||||
proxy_set_header X-Original-URI $request_uri;
|
||||
|
@ -41,18 +37,12 @@ http {
|
|||
proxy_set_header X-Real-IP $remote_addr;
|
||||
|
||||
proxy_pass http://auth/;
|
||||
}
|
||||
|
||||
location /js/ {
|
||||
proxy_pass http://auth/js/;
|
||||
}
|
||||
proxy_intercept_errors on;
|
||||
|
||||
location /img/ {
|
||||
proxy_pass http://auth/img/;
|
||||
}
|
||||
|
||||
location /css/ {
|
||||
proxy_pass http://auth/css/;
|
||||
error_page 401 = /error/401;
|
||||
error_page 403 = /error/403;
|
||||
error_page 404 = /error/404;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -61,8 +51,7 @@ http {
|
|||
root /usr/share/nginx/html;
|
||||
|
||||
server_name secret1.test.local secret2.test.local secret.test.local
|
||||
home.test.local mx1.mail.test.local mx2.mail.test.local
|
||||
localhost;
|
||||
home.test.local mx1.mail.test.local mx2.mail.test.local;
|
||||
|
||||
ssl on;
|
||||
ssl_certificate /etc/ssl/server.crt;
|
||||
|
@ -70,7 +59,7 @@ http {
|
|||
|
||||
error_page 401 = @error401;
|
||||
location @error401 {
|
||||
return 302 https://auth.test.local:8080/login?redirect=$scheme://$http_host$request_uri;
|
||||
return 302 https://auth.test.local:8080;
|
||||
}
|
||||
|
||||
location /auth_verify {
|
||||
|
|
After Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 2.0 KiB |
BIN
images/totp.png
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 27 KiB |
BIN
images/u2f.png
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 22 KiB |
61
package.json
|
@ -1,20 +1,18 @@
|
|||
{
|
||||
"name": "authelia",
|
||||
"version": "2.1.9",
|
||||
"description": "2-factor authentication server using LDAP as 1st factor and TOTP or U2F as 2nd factor",
|
||||
"description": "2FA Single Sign-On server for nginx using LDAP, TOTP and U2F",
|
||||
"main": "src/index.js",
|
||||
"bin": {
|
||||
"authelia": "src/index.js"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "./node_modules/.bin/mocha --compilers ts:ts-node/register --recursive test/unitary",
|
||||
"test-dbg": "./node_modules/.bin/mocha --debug-brk --compilers ts:ts-node/register --recursive test/unitary",
|
||||
"int-test": "./node_modules/.bin/mocha --recursive test/integration",
|
||||
"coverage": "./node_modules/.bin/istanbul cover _mocha -- -R spec --recursive test",
|
||||
"build-ts": "tsc",
|
||||
"watch-ts": "tsc -w",
|
||||
"test": "./node_modules/.bin/mocha --compilers ts:ts-node/register --recursive test/client test/server",
|
||||
"int-test": "./node_modules/.bin/mocha --compilers ts:ts-node/register --recursive test/integration",
|
||||
"cover": "NODE_ENV=test nyc npm t",
|
||||
"build": "tsc",
|
||||
"tslint": "tslint -c tslint.json -p tsconfig.json",
|
||||
"serve": "node dist/src/index.js"
|
||||
"serve": "node dist/server/index.js"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -29,7 +27,7 @@
|
|||
"title": "Authelia API documentation"
|
||||
},
|
||||
"dependencies": {
|
||||
"authdog": "^0.1.1",
|
||||
"@types/cors": "^2.8.1",
|
||||
"bluebird": "^3.4.7",
|
||||
"body-parser": "^1.15.2",
|
||||
"dovehash": "0.0.5",
|
||||
|
@ -40,8 +38,10 @@
|
|||
"nedb": "^1.8.0",
|
||||
"nodemailer": "^2.7.0",
|
||||
"object-path": "^0.11.3",
|
||||
"pug": "^2.0.0-rc.2",
|
||||
"randomstring": "^1.1.5",
|
||||
"speakeasy": "^2.0.0",
|
||||
"u2f": "^0.1.2",
|
||||
"winston": "^2.3.1",
|
||||
"yamljs": "^0.2.8"
|
||||
},
|
||||
|
@ -52,6 +52,8 @@
|
|||
"@types/ejs": "^2.3.33",
|
||||
"@types/express": "^4.0.35",
|
||||
"@types/express-session": "0.0.32",
|
||||
"@types/jquery": "^2.0.45",
|
||||
"@types/jsdom": "^2.0.30",
|
||||
"@types/ldapjs": "^1.0.0",
|
||||
"@types/mocha": "^2.2.41",
|
||||
"@types/mockdate": "^2.0.0",
|
||||
|
@ -59,6 +61,7 @@
|
|||
"@types/nodemailer": "^1.3.32",
|
||||
"@types/object-path": "^0.9.28",
|
||||
"@types/proxyquire": "^1.3.27",
|
||||
"@types/query-string": "^4.3.1",
|
||||
"@types/randomstring": "^1.1.5",
|
||||
"@types/request": "0.0.43",
|
||||
"@types/sinon": "^2.2.1",
|
||||
|
@ -66,12 +69,25 @@
|
|||
"@types/tmp": "0.0.33",
|
||||
"@types/winston": "^2.3.2",
|
||||
"@types/yamljs": "^0.2.30",
|
||||
"apidoc": "^0.17.6",
|
||||
"browserify": "^14.3.0",
|
||||
"grunt": "^1.0.1",
|
||||
"grunt-browserify": "^5.0.0",
|
||||
"grunt-contrib-concat": "^1.0.1",
|
||||
"grunt-contrib-copy": "^1.0.0",
|
||||
"grunt-contrib-cssmin": "^2.2.0",
|
||||
"grunt-contrib-watch": "^1.0.0",
|
||||
"grunt-run": "^0.6.0",
|
||||
"istanbul": "^0.4.5",
|
||||
"jquery": "^3.2.1",
|
||||
"js-logger": "^1.3.0",
|
||||
"jsdom": "^11.0.0",
|
||||
"mocha": "^3.2.0",
|
||||
"mockdate": "^2.0.1",
|
||||
"notifyjs-browser": "^0.4.2",
|
||||
"nyc": "^10.3.2",
|
||||
"proxyquire": "^1.8.0",
|
||||
"query-string": "^4.3.4",
|
||||
"request": "^2.79.0",
|
||||
"should": "^11.1.1",
|
||||
"sinon": "^1.17.6",
|
||||
|
@ -79,6 +95,31 @@
|
|||
"tmp": "0.0.31",
|
||||
"ts-node": "^3.0.4",
|
||||
"tslint": "^5.2.0",
|
||||
"typescript": "^2.3.2"
|
||||
"typescript": "^2.3.2",
|
||||
"u2f-api": "0.0.9",
|
||||
"uglify-es": "^3.0.15"
|
||||
},
|
||||
"nyc": {
|
||||
"include": [
|
||||
"src/*.ts",
|
||||
"src/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"doc",
|
||||
"src/types",
|
||||
"dist",
|
||||
"test"
|
||||
],
|
||||
"extension": [
|
||||
".ts"
|
||||
],
|
||||
"require": [
|
||||
"ts-node/register"
|
||||
],
|
||||
"reporter": [
|
||||
"json",
|
||||
"html"
|
||||
],
|
||||
"all": true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
|
||||
body {
|
||||
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI1NiIgaGVpZ2h0PSIxMDAiPgo8cmVjdCB3aWR0aD0iNTYiIGhlaWdodD0iMTAwIiBmaWxsPSIjRkZGRkZGIj48L3JlY3Q+CjxwYXRoIGQ9Ik0yOCA2NkwwIDUwTDAgMTZMMjggMEw1NiAxNkw1NiA1MEwyOCA2NkwyOCAxMDAiIGZpbGw9Im5vbmUiIHN0cm9rZT0iI0ZDRkNGQyIgc3Ryb2tlLXdpZHRoPSIyIj48L3BhdGg+CjxwYXRoIGQ9Ik0yOCAwTDI4IDM0TDAgNTBMMCA4NEwyOCAxMDBMNTYgODRMNTYgNTBMMjggMzQiIGZpbGw9Im5vbmUiIHN0cm9rZT0iI0ZCRkJGQiIgc3Ryb2tlLXdpZHRoPSIyIj48L3BhdGg+Cjwvc3ZnPg==");
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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%;
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
|
||||
.password-reset-form .header-img {
|
||||
border-radius: 0%;
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
|
||||
.password-reset-request .header-img {
|
||||
border-radius: 0%;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
|
||||
.u2f-register img {
|
||||
display: block;
|
||||
margin: 20px auto;
|
||||
}
|
|
@ -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));
|
||||
});
|
||||
});
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
|
||||
export const USERNAME_FIELD_ID = "#username";
|
||||
export const PASSWORD_FIELD_ID = "#password";
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
After Width: | Height: | Size: 814 B |
After Width: | Height: | Size: 3.5 KiB |
After Width: | Height: | Size: 3.2 KiB |
After Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 6.6 KiB |
After Width: | Height: | Size: 3.1 KiB |
After Width: | Height: | Size: 2.9 KiB |
After Width: | Height: | Size: 3.9 KiB |
|
@ -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);
|
||||
}
|
||||
};
|
|
@ -0,0 +1,2 @@
|
|||
|
||||
export const FORM_SELECTOR = ".form-signin";
|
|
@ -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);
|
||||
});
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
@ -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));
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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";
|
|
@ -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);
|
||||
});
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
|
||||
export const QRCODE_ID_SELECTOR = "#qrcode";
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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();
|
||||
});
|
||||
};
|
||||
|
|
@ -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);
|
||||
};
|
||||
};
|
|
@ -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");
|
||||
});
|
||||
};
|
|
@ -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)
|
||||
};
|
|
@ -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;
|
||||
|
|
@ -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");
|
||||
});
|
||||
};
|
|
@ -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,
|
||||
};
|
|
@ -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
|
||||
};
|
|
@ -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(),
|
||||
};
|
||||
|
|
@ -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
|
||||
};
|
|
@ -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;
|
|
@ -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
|
||||
};
|
|
@ -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;
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
|
||||
})();
|
|
@ -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);
|
||||
});
|
||||
|
||||
})();
|
|
@ -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);
|
||||
});
|
||||
|
||||
})();
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
})();
|
|
@ -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);
|
||||
});
|
||||
};
|
|
@ -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);
|
||||
});
|
||||
|
||||
})();
|
|
@ -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";
|
|
@ -17,7 +17,7 @@ console.log("Parse configuration file: %s", config_path);
|
|||
const yaml_config = YAML.load(config_path);
|
||||
|
||||
const deps = {
|
||||
u2f: require("authdog"),
|
||||
u2f: require("u2f"),
|
||||
nodemailer: require("nodemailer"),
|
||||
ldapjs: require("ldapjs"),
|
||||
session: require("express-session"),
|
|
@ -9,7 +9,7 @@ interface DatedDocument {
|
|||
date: Date;
|
||||
}
|
||||
|
||||
export default class AuthenticationRegulator {
|
||||
export class AuthenticationRegulator {
|
||||
private _user_data_store: any;
|
||||
private _lock_time_in_seconds: number;
|
||||
|
|
@ -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;
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
|
||||
import * as ObjectPath from "object-path";
|
||||
import { AppConfiguration, UserConfiguration, NotifierConfiguration, ACLConfiguration, LdapConfiguration } from "./Configuration";
|
||||
import { AppConfiguration, UserConfiguration, NotifierConfiguration, ACLConfiguration, LdapConfiguration } from "./../../types/Configuration";
|
||||
|
||||
|
||||
function get_optional<T>(config: object, path: string, default_value: T): T {
|
|
@ -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);
|
||||
}
|
|
@ -1,9 +1,9 @@
|
|||
|
||||
export class LdapSeachError extends Error {
|
||||
export class LdapSearchError extends Error {
|
||||
constructor(message?: string) {
|
||||
super(message);
|
||||
this.name = "LdapSeachError";
|
||||
Object.setPrototypeOf(this, LdapSeachError.prototype);
|
||||
this.name = "LdapSearchError";
|
||||
Object.setPrototypeOf(this, LdapSearchError.prototype);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -54,3 +54,19 @@ export class DomainAccessDenied extends Error {
|
|||
Object.setPrototypeOf(this, DomainAccessDenied.prototype);
|
||||
}
|
||||
}
|
||||
|
||||
export class FirstFactorValidationError extends Error {
|
||||
constructor(message?: string) {
|
||||
super(message);
|
||||
this.name = "FirstFactorValidationError";
|
||||
Object.setPrototypeOf(this, FirstFactorValidationError.prototype);
|
||||
}
|
||||
}
|
||||
|
||||
export class SecondFactorValidationError extends Error {
|
||||
constructor(message?: string) {
|
||||
super(message);
|
||||
this.name = "SecondFactorValidationError";
|
||||
Object.setPrototypeOf(this, FirstFactorValidationError.prototype);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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));
|
||||
};
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
|
||||
|
||||
export const PRE_VALIDATION_TEMPLATE = "need-identity-validation";
|
|
@ -6,21 +6,21 @@ import Dovehash = require("dovehash");
|
|||
import ldapjs = require("ldapjs");
|
||||
|
||||
import { EventEmitter } from "events";
|
||||
import { LdapConfiguration } from "./Configuration";
|
||||
import { Ldapjs } from "../types/Dependencies";
|
||||
import { ILogger } from "../types/ILogger";
|
||||
import { LdapConfiguration } from "./../../types/Configuration";
|
||||
import { Ldapjs } from "../../types/Dependencies";
|
||||
import { Winston } from "../../types/Dependencies";
|
||||
|
||||
interface SearchEntry {
|
||||
object: any;
|
||||
}
|
||||
|
||||
export class LdapClient {
|
||||
options: LdapConfiguration;
|
||||
ldapjs: Ldapjs;
|
||||
logger: ILogger;
|
||||
client: ldapjs.ClientAsync;
|
||||
private options: LdapConfiguration;
|
||||
private ldapjs: Ldapjs;
|
||||
private logger: Winston;
|
||||
private client: ldapjs.ClientAsync;
|
||||
|
||||
constructor(options: LdapConfiguration, ldapjs: Ldapjs, logger: ILogger) {
|
||||
constructor(options: LdapConfiguration, ldapjs: Ldapjs, logger: Winston) {
|
||||
this.options = options;
|
||||
this.ldapjs = ldapjs;
|
||||
this.logger = logger;
|
||||
|
@ -60,7 +60,7 @@ export class LdapClient {
|
|||
|
||||
this.logger.debug("LDAP: Bind user %s", user_dn);
|
||||
return this.client.bindAsync(user_dn, password)
|
||||
.error(function (err) {
|
||||
.error(function (err: Error) {
|
||||
throw new exceptions.LdapBindError(err.message);
|
||||
});
|
||||
}
|
||||
|
@ -75,14 +75,14 @@ export class LdapClient {
|
|||
doc.push(entry.object);
|
||||
});
|
||||
res.on("error", function (err: Error) {
|
||||
reject(err);
|
||||
reject(new exceptions.LdapSearchError(err.message));
|
||||
});
|
||||
res.on("end", function () {
|
||||
resolve(doc);
|
||||
});
|
||||
})
|
||||
.catch(function (err) {
|
||||
reject(err);
|
||||
.catch(function (err: Error) {
|
||||
reject(new exceptions.LdapSearchError(err.message));
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
|
@ -1,16 +1,16 @@
|
|||
|
||||
import * as speakeasy from "speakeasy";
|
||||
import { Speakeasy } from "../types/Dependencies";
|
||||
import { Speakeasy } from "../../types/Dependencies";
|
||||
import BluebirdPromise = require("bluebird");
|
||||
|
||||
export default class TOTPGenerator {
|
||||
export class TOTPGenerator {
|
||||
private speakeasy: Speakeasy;
|
||||
|
||||
constructor(speakeasy: Speakeasy) {
|
||||
this.speakeasy = speakeasy;
|
||||
}
|
||||
|
||||
generate(options: speakeasy.GenerateOptions): speakeasy.Key {
|
||||
generate(options?: speakeasy.GenerateOptions): speakeasy.Key {
|
||||
return this.speakeasy.generateSecret(options);
|
||||
}
|
||||
}
|
|
@ -1,10 +1,10 @@
|
|||
|
||||
import { Speakeasy } from "../types/Dependencies";
|
||||
import { Speakeasy } from "../../types/Dependencies";
|
||||
import BluebirdPromise = require("bluebird");
|
||||
|
||||
const TOTP_ENCODING = "base32";
|
||||
|
||||
export default class TOTPValidator {
|
||||
export class TOTPValidator {
|
||||
private speakeasy: Speakeasy;
|
||||
|
||||
constructor(speakeasy: Speakeasy) {
|