Refactor client to make it responsive and testable
5
.gitignore
vendored
|
@ -2,6 +2,9 @@
|
||||||
# NodeJs modules
|
# NodeJs modules
|
||||||
node_modules/
|
node_modules/
|
||||||
|
|
||||||
|
# npm debug logs
|
||||||
|
npm-debug.log*
|
||||||
|
|
||||||
# Coverage reports
|
# Coverage reports
|
||||||
coverage/
|
coverage/
|
||||||
|
|
||||||
|
@ -24,3 +27,5 @@ notifications/
|
||||||
|
|
||||||
# Generated by TypeScript compiler
|
# Generated by TypeScript compiler
|
||||||
dist/
|
dist/
|
||||||
|
|
||||||
|
.nyc_output/
|
||||||
|
|
|
@ -20,7 +20,7 @@ addons:
|
||||||
before_install: npm install -g npm@'>=2.13.5'
|
before_install: npm install -g npm@'>=2.13.5'
|
||||||
script:
|
script:
|
||||||
- grunt test
|
- grunt test
|
||||||
- grunt build
|
- grunt dist
|
||||||
- grunt docker-build
|
- grunt docker-build
|
||||||
- docker-compose build
|
- docker-compose build
|
||||||
- docker-compose up -d
|
- docker-compose up -d
|
||||||
|
|
|
@ -5,7 +5,7 @@ WORKDIR /usr/src
|
||||||
COPY package.json /usr/src/package.json
|
COPY package.json /usr/src/package.json
|
||||||
RUN npm install --production
|
RUN npm install --production
|
||||||
|
|
||||||
COPY dist/src /usr/src
|
COPY dist/src/server /usr/src
|
||||||
|
|
||||||
ENV PORT=80
|
ENV PORT=80
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
|
|
127
Gruntfile.js
|
@ -1,10 +1,12 @@
|
||||||
module.exports = function(grunt) {
|
module.exports = function (grunt) {
|
||||||
|
const buildDir = "dist";
|
||||||
|
|
||||||
grunt.initConfig({
|
grunt.initConfig({
|
||||||
run: {
|
run: {
|
||||||
options: {},
|
options: {},
|
||||||
"build-ts": {
|
"build": {
|
||||||
cmd: "npm",
|
cmd: "npm",
|
||||||
args: ['run', 'build-ts']
|
args: ['run', 'build']
|
||||||
},
|
},
|
||||||
"tslint": {
|
"tslint": {
|
||||||
cmd: "npm",
|
cmd: "npm",
|
||||||
|
@ -17,39 +19,136 @@ module.exports = function(grunt) {
|
||||||
"docker-build": {
|
"docker-build": {
|
||||||
cmd: "docker",
|
cmd: "docker",
|
||||||
args: ['build', '-t', 'clems4ever/authelia', '.']
|
args: ['build', '-t', 'clems4ever/authelia', '.']
|
||||||
|
},
|
||||||
|
"docker-restart": {
|
||||||
|
cmd: "docker-compose",
|
||||||
|
args: ['-f', 'docker-compose.yml', '-f', 'docker-compose.dev.yml', 'restart', 'auth']
|
||||||
|
},
|
||||||
|
"minify": {
|
||||||
|
cmd: "./node_modules/.bin/uglifyjs",
|
||||||
|
args: [`${buildDir}/src/server/public_html/js/authelia.js`, '-o', `${buildDir}/src/server/public_html/js/authelia.min.js`]
|
||||||
|
},
|
||||||
|
"apidoc": {
|
||||||
|
cmd: "./node_modules/.bin/apidoc",
|
||||||
|
args: ["-i", "src/server", "-o", "doc"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
copy: {
|
copy: {
|
||||||
resources: {
|
resources: {
|
||||||
expand: true,
|
expand: true,
|
||||||
cwd: 'src/resources/',
|
cwd: 'src/server/resources/',
|
||||||
src: '**',
|
src: '**',
|
||||||
dest: 'dist/src/resources/'
|
dest: `${buildDir}/src/server/resources/`
|
||||||
},
|
},
|
||||||
views: {
|
views: {
|
||||||
expand: true,
|
expand: true,
|
||||||
cwd: 'src/views/',
|
cwd: 'src/server/views/',
|
||||||
src: '**',
|
src: '**',
|
||||||
dest: 'dist/src/views/'
|
dest: `${buildDir}/src/server/views/`
|
||||||
},
|
},
|
||||||
public_html: {
|
images: {
|
||||||
expand: true,
|
expand: true,
|
||||||
cwd: 'src/public_html/',
|
cwd: 'src/client/img',
|
||||||
src: '**',
|
src: '**',
|
||||||
dest: 'dist/src/public_html/'
|
dest: `${buildDir}/src/server/public_html/img/`
|
||||||
|
},
|
||||||
|
thirdparties: {
|
||||||
|
expand: true,
|
||||||
|
cwd: 'src/client/thirdparties',
|
||||||
|
src: '**',
|
||||||
|
dest: `${buildDir}/src/server/public_html/js/`
|
||||||
|
},
|
||||||
|
},
|
||||||
|
browserify: {
|
||||||
|
dist: {
|
||||||
|
src: ['dist/src/client/index.js'],
|
||||||
|
dest: `${buildDir}/src/server/public_html/js/authelia.js`,
|
||||||
|
options: {
|
||||||
|
browserifyOptions: {
|
||||||
|
standalone: 'authelia'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
views: {
|
||||||
|
files: ['src/server/views/**/*.pug'],
|
||||||
|
tasks: ['copy:views'],
|
||||||
|
options: {
|
||||||
|
interrupt: false,
|
||||||
|
atBegin: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
resources: {
|
||||||
|
files: ['src/server/resources/*.ejs'],
|
||||||
|
tasks: ['copy:resources'],
|
||||||
|
options: {
|
||||||
|
interrupt: false,
|
||||||
|
atBegin: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
images: {
|
||||||
|
files: ['src/client/img/**'],
|
||||||
|
tasks: ['copy:images'],
|
||||||
|
options: {
|
||||||
|
interrupt: false,
|
||||||
|
atBegin: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
css: {
|
||||||
|
files: ['src/client/**/*.css'],
|
||||||
|
tasks: ['concat:css', 'cssmin'],
|
||||||
|
options: {
|
||||||
|
interrupt: true,
|
||||||
|
atBegin: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
client: {
|
||||||
|
files: ['src/client/**/*.ts', 'test/client/**/*.ts'],
|
||||||
|
tasks: ['build'],
|
||||||
|
options: {
|
||||||
|
interrupt: true,
|
||||||
|
atBegin: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
files: ['src/server/**/*.ts', 'test/server/**/*.ts'],
|
||||||
|
tasks: ['build', 'run:docker-restart'],
|
||||||
|
options: {
|
||||||
|
interrupt: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
concat: {
|
||||||
|
css: {
|
||||||
|
src: ['src/client/css/*.css'],
|
||||||
|
dest: `${buildDir}/src/server/public_html/css/authelia.css`
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cssmin: {
|
||||||
|
target: {
|
||||||
|
files: {
|
||||||
|
[`${buildDir}/src/server/public_html/css/authelia.min.css`]: [`${buildDir}/src/server/public_html/css/authelia.css`]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
grunt.loadNpmTasks('grunt-browserify');
|
||||||
|
grunt.loadNpmTasks('grunt-contrib-concat');
|
||||||
|
grunt.loadNpmTasks('grunt-contrib-copy');
|
||||||
|
grunt.loadNpmTasks('grunt-contrib-cssmin');
|
||||||
|
grunt.loadNpmTasks('grunt-contrib-watch');
|
||||||
grunt.loadNpmTasks('grunt-run');
|
grunt.loadNpmTasks('grunt-run');
|
||||||
grunt.loadNpmTasks('grunt-contrib-copy');
|
|
||||||
|
|
||||||
grunt.registerTask('default', ['build']);
|
grunt.registerTask('default', ['build']);
|
||||||
|
|
||||||
grunt.registerTask('res', ['copy:resources', 'copy:views', 'copy:public_html']);
|
|
||||||
|
|
||||||
grunt.registerTask('build', ['run:tslint', 'run:build-ts', 'res']);
|
grunt.registerTask('build-resources', ['copy:resources', 'copy:views', 'copy:images', 'copy:thirdparties', 'concat:css', 'cssmin']);
|
||||||
|
grunt.registerTask('build', ['run:tslint', 'run:build', 'browserify:dist']);
|
||||||
|
grunt.registerTask('dist', ['build', 'build-resources', 'run:minify', 'cssmin']);
|
||||||
|
|
||||||
grunt.registerTask('docker-build', ['run:docker-build']);
|
grunt.registerTask('docker-build', ['run:docker-build']);
|
||||||
|
grunt.registerTask('docker-restart', ['run:docker-restart']);
|
||||||
|
|
||||||
grunt.registerTask('test', ['run:test']);
|
grunt.registerTask('test', ['run:test']);
|
||||||
};
|
};
|
||||||
|
|
|
@ -117,6 +117,8 @@ email address. For the sake of the example, the email is delivered in the file
|
||||||
./notifications/notification.txt.
|
./notifications/notification.txt.
|
||||||
Paste the link in your browser and you should be able to reset the password.
|
Paste the link in your browser and you should be able to reset the password.
|
||||||
|
|
||||||
|
![reset-password](https://raw.githubusercontent.com/clems4ever/authelia/master/images/reset_password.png)
|
||||||
|
|
||||||
### Access Control
|
### Access Control
|
||||||
With **Authelia**, you can define your own access control rules for restricting
|
With **Authelia**, you can define your own access control rules for restricting
|
||||||
the access to certain subdomains to your users. Those rules are defined in the
|
the access to certain subdomains to your users. Those rules are defined in the
|
||||||
|
|
|
@ -76,7 +76,7 @@ session:
|
||||||
|
|
||||||
|
|
||||||
# The directory where the DB files will be saved
|
# The directory where the DB files will be saved
|
||||||
store_directory: /var/lib/auth-server/store
|
store_directory: /var/lib/authelia/store
|
||||||
|
|
||||||
|
|
||||||
# Notifications are sent to users when they require a password reset, a u2f
|
# Notifications are sent to users when they require a password reset, a u2f
|
||||||
|
|
1078
doc/api_data.js
1078
doc/api_data.json
|
@ -1,15 +1,15 @@
|
||||||
define({
|
define({
|
||||||
"title": "Authelia API documentation",
|
"title": "Authelia API documentation",
|
||||||
"name": "authelia",
|
"name": "authelia",
|
||||||
"version": "1.0.11",
|
"version": "2.1.3",
|
||||||
"description": "2-factor authentication server using LDAP as 1st factor and TOTP or U2F as 2nd factor",
|
"description": "2FA Single Sign-On server for nginx using LDAP, TOTP and U2F",
|
||||||
"sampleUrl": false,
|
"sampleUrl": false,
|
||||||
"defaultVersion": "0.0.0",
|
"defaultVersion": "0.0.0",
|
||||||
"apidoc": "0.3.0",
|
"apidoc": "0.3.0",
|
||||||
"generator": {
|
"generator": {
|
||||||
"name": "apidoc",
|
"name": "apidoc",
|
||||||
"time": "2017-01-29T00:44:17.687Z",
|
"time": "2017-06-11T20:41:36.025Z",
|
||||||
"url": "http://apidocjs.com",
|
"url": "http://apidocjs.com",
|
||||||
"version": "0.17.5"
|
"version": "0.17.6"
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
{
|
{
|
||||||
"title": "Authelia API documentation",
|
"title": "Authelia API documentation",
|
||||||
"name": "authelia",
|
"name": "authelia",
|
||||||
"version": "1.0.11",
|
"version": "2.1.3",
|
||||||
"description": "2-factor authentication server using LDAP as 1st factor and TOTP or U2F as 2nd factor",
|
"description": "2FA Single Sign-On server for nginx using LDAP, TOTP and U2F",
|
||||||
"sampleUrl": false,
|
"sampleUrl": false,
|
||||||
"defaultVersion": "0.0.0",
|
"defaultVersion": "0.0.0",
|
||||||
"apidoc": "0.3.0",
|
"apidoc": "0.3.0",
|
||||||
"generator": {
|
"generator": {
|
||||||
"name": "apidoc",
|
"name": "apidoc",
|
||||||
"time": "2017-01-29T00:44:17.687Z",
|
"time": "2017-06-11T20:41:36.025Z",
|
||||||
"url": "http://apidocjs.com",
|
"url": "http://apidocjs.com",
|
||||||
"version": "0.17.5"
|
"version": "0.17.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -172,6 +172,7 @@ pre {
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
position: relative;
|
position: relative;
|
||||||
margin: 10px 0 20px 0;
|
margin: 10px 0 20px 0;
|
||||||
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
pre.prettyprint {
|
pre.prettyprint {
|
||||||
|
|
|
@ -224,7 +224,7 @@
|
||||||
<div class="tab-content">
|
<div class="tab-content">
|
||||||
{{#each params.examples}}
|
{{#each params.examples}}
|
||||||
<div class="tab-pane{{#if_eq @index compare=0}} active{{/if_eq}}" id="{{../section}}-examples-{{../id}}-{{@index}}">
|
<div class="tab-pane{{#if_eq @index compare=0}} active{{/if_eq}}" id="{{../section}}-examples-{{../id}}-{{@index}}">
|
||||||
<pre class="prettyprint language-{{type}}" data-type="{{type}}"><code>{{{reformat content type}}}</code></pre>
|
<pre class="prettyprint language-{{type}}" data-type="{{type}}"><code>{{reformat content type}}</code></pre>
|
||||||
</div>
|
</div>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</div>
|
</div>
|
||||||
|
@ -274,7 +274,7 @@
|
||||||
{{#each this}}
|
{{#each this}}
|
||||||
<label class="col-md-3 control-label" for="sample-request-param-field-{{field}}">{{field}}</label>
|
<label class="col-md-3 control-label" for="sample-request-param-field-{{field}}">{{field}}</label>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input id="sample-request-param-field-{{field}}" type="text" placeholder="{{field}}" class="form-control sample-request-param" data-sample-request-param-name="{{field}}" data-sample-request-param-group="sample-request-param-{{@../index}}">
|
<input id="sample-request-param-field-{{field}}" type="text" placeholder="{{field}}" class="form-control sample-request-param" data-sample-request-param-name="{{field}}" data-sample-request-param-group="sample-request-param-{{@../index}}" {{#if optional}}data-sample-request-param-optional="true"{{/if}}>
|
||||||
<div class="input-group-addon">{{{type}}}</div>
|
<div class="input-group-addon">{{{type}}}</div>
|
||||||
</div>
|
</div>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
|
|
|
@ -9,6 +9,8 @@ define([
|
||||||
'./locales/pt_br.js',
|
'./locales/pt_br.js',
|
||||||
'./locales/ro.js',
|
'./locales/ro.js',
|
||||||
'./locales/ru.js',
|
'./locales/ru.js',
|
||||||
|
'./locales/tr.js',
|
||||||
|
'./locales/vi.js',
|
||||||
'./locales/zh.js',
|
'./locales/zh.js',
|
||||||
'./locales/zh_cn.js'
|
'./locales/zh_cn.js'
|
||||||
], function() {
|
], function() {
|
||||||
|
|
25
doc/locales/tr.js
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
define({
|
||||||
|
tr: {
|
||||||
|
'Allowed values:' : 'İzin verilen değerler:',
|
||||||
|
'Compare all with predecessor': 'Tümünü öncekiler ile karşılaştır',
|
||||||
|
'compare changes to:' : 'değişiklikleri karşılaştır:',
|
||||||
|
'compared to' : 'karşılaştır',
|
||||||
|
'Default value:' : 'Varsayılan değer:',
|
||||||
|
'Description' : 'Açıklama',
|
||||||
|
'Field' : 'Alan',
|
||||||
|
'General' : 'Genel',
|
||||||
|
'Generated with' : 'Oluşturan',
|
||||||
|
'Name' : 'İsim',
|
||||||
|
'No response values.' : 'Dönüş verisi yok.',
|
||||||
|
'optional' : 'opsiyonel',
|
||||||
|
'Parameter' : 'Parametre',
|
||||||
|
'Permission:' : 'İzin:',
|
||||||
|
'Response' : 'Dönüş',
|
||||||
|
'Send' : 'Gönder',
|
||||||
|
'Send a Sample Request' : 'Örnek istek gönder',
|
||||||
|
'show up to version:' : 'bu versiyona kadar göster:',
|
||||||
|
'Size range:' : 'Boyut aralığı:',
|
||||||
|
'Type' : 'Tip',
|
||||||
|
'url' : 'url'
|
||||||
|
}
|
||||||
|
});
|
25
doc/locales/vi.js
Normal file
|
@ -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 = {};
|
var paramType = {};
|
||||||
$root.find(".sample-request-param:checked").each(function(i, element) {
|
$root.find(".sample-request-param:checked").each(function(i, element) {
|
||||||
var group = $(element).data("sample-request-param-group-id");
|
var group = $(element).data("sample-request-param-group-id");
|
||||||
$root.find("[data-sample-request-param-group=\"" + group + "\"]").each(function(i, element) {
|
$root.find("[data-sample-request-param-group=\"" + group + "\"]").not(function(){
|
||||||
|
return $(this).val() == "" && $(this).is("[data-sample-request-param-optional='true']");
|
||||||
|
}).each(function(i, element) {
|
||||||
var key = $(element).data("sample-request-param-name");
|
var key = $(element).data("sample-request-param-name");
|
||||||
var value = element.value;
|
var value = element.value;
|
||||||
if ( ! element.optional && element.defaultValue !== '') {
|
if ( ! element.optional && element.defaultValue !== '') {
|
||||||
|
|
|
@ -4,8 +4,8 @@ services:
|
||||||
auth:
|
auth:
|
||||||
volumes:
|
volumes:
|
||||||
- ./test:/usr/src/test
|
- ./test:/usr/src/test
|
||||||
- ./src/views:/usr/src/views
|
- ./dist/src/server:/usr/src
|
||||||
- ./src/public_html:/usr/src/public_html
|
- ./node_modules:/usr/src/node_modules
|
||||||
- ./config.yml:/etc/auth-server/config.yml:ro
|
- ./config.yml:/etc/auth-server/config.yml:ro
|
||||||
|
|
||||||
ldap-admin:
|
ldap-admin:
|
||||||
|
|
|
@ -25,7 +25,7 @@ dn: cn=john,ou=users,dc=example,dc=com
|
||||||
cn: john
|
cn: john
|
||||||
objectclass: inetOrgPerson
|
objectclass: inetOrgPerson
|
||||||
objectclass: top
|
objectclass: top
|
||||||
mail: john.doe@example.com
|
mail: clement.michaud34@gmail.com
|
||||||
sn: John Doe
|
sn: John Doe
|
||||||
userpassword: {SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g=
|
userpassword: {SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g=
|
||||||
|
|
||||||
|
|
|
@ -30,10 +30,6 @@ http {
|
||||||
ssl_certificate /etc/ssl/server.crt;
|
ssl_certificate /etc/ssl/server.crt;
|
||||||
ssl_certificate_key /etc/ssl/server.key;
|
ssl_certificate_key /etc/ssl/server.key;
|
||||||
|
|
||||||
error_page 401 = @error401;
|
|
||||||
location @error401 {
|
|
||||||
return 302 https://auth.test.local:8080/login?redirect=$scheme://$http_host$request_uri;
|
|
||||||
}
|
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
proxy_set_header X-Original-URI $request_uri;
|
proxy_set_header X-Original-URI $request_uri;
|
||||||
|
@ -41,18 +37,12 @@ http {
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
|
||||||
proxy_pass http://auth/;
|
proxy_pass http://auth/;
|
||||||
}
|
|
||||||
|
|
||||||
location /js/ {
|
proxy_intercept_errors on;
|
||||||
proxy_pass http://auth/js/;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /img/ {
|
error_page 401 = /error/401;
|
||||||
proxy_pass http://auth/img/;
|
error_page 403 = /error/403;
|
||||||
}
|
error_page 404 = /error/404;
|
||||||
|
|
||||||
location /css/ {
|
|
||||||
proxy_pass http://auth/css/;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,8 +51,7 @@ http {
|
||||||
root /usr/share/nginx/html;
|
root /usr/share/nginx/html;
|
||||||
|
|
||||||
server_name secret1.test.local secret2.test.local secret.test.local
|
server_name secret1.test.local secret2.test.local secret.test.local
|
||||||
home.test.local mx1.mail.test.local mx2.mail.test.local
|
home.test.local mx1.mail.test.local mx2.mail.test.local;
|
||||||
localhost;
|
|
||||||
|
|
||||||
ssl on;
|
ssl on;
|
||||||
ssl_certificate /etc/ssl/server.crt;
|
ssl_certificate /etc/ssl/server.crt;
|
||||||
|
@ -70,7 +59,7 @@ http {
|
||||||
|
|
||||||
error_page 401 = @error401;
|
error_page 401 = @error401;
|
||||||
location @error401 {
|
location @error401 {
|
||||||
return 302 https://auth.test.local:8080/login?redirect=$scheme://$http_host$request_uri;
|
return 302 https://auth.test.local:8080;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /auth_verify {
|
location /auth_verify {
|
||||||
|
|
BIN
images/email_confirmation.png
Normal file
After Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 20 KiB |
BIN
images/reset_password.png
Normal file
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",
|
"name": "authelia",
|
||||||
"version": "2.1.9",
|
"version": "2.1.9",
|
||||||
"description": "2-factor authentication server using LDAP as 1st factor and TOTP or U2F as 2nd factor",
|
"description": "2FA Single Sign-On server for nginx using LDAP, TOTP and U2F",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"bin": {
|
"bin": {
|
||||||
"authelia": "src/index.js"
|
"authelia": "src/index.js"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "./node_modules/.bin/mocha --compilers ts:ts-node/register --recursive test/unitary",
|
"test": "./node_modules/.bin/mocha --compilers ts:ts-node/register --recursive test/client test/server",
|
||||||
"test-dbg": "./node_modules/.bin/mocha --debug-brk --compilers ts:ts-node/register --recursive test/unitary",
|
"int-test": "./node_modules/.bin/mocha --compilers ts:ts-node/register --recursive test/integration",
|
||||||
"int-test": "./node_modules/.bin/mocha --recursive test/integration",
|
"cover": "NODE_ENV=test nyc npm t",
|
||||||
"coverage": "./node_modules/.bin/istanbul cover _mocha -- -R spec --recursive test",
|
"build": "tsc",
|
||||||
"build-ts": "tsc",
|
|
||||||
"watch-ts": "tsc -w",
|
|
||||||
"tslint": "tslint -c tslint.json -p tsconfig.json",
|
"tslint": "tslint -c tslint.json -p tsconfig.json",
|
||||||
"serve": "node dist/src/index.js"
|
"serve": "node dist/server/index.js"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
@ -29,7 +27,7 @@
|
||||||
"title": "Authelia API documentation"
|
"title": "Authelia API documentation"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"authdog": "^0.1.1",
|
"@types/cors": "^2.8.1",
|
||||||
"bluebird": "^3.4.7",
|
"bluebird": "^3.4.7",
|
||||||
"body-parser": "^1.15.2",
|
"body-parser": "^1.15.2",
|
||||||
"dovehash": "0.0.5",
|
"dovehash": "0.0.5",
|
||||||
|
@ -40,8 +38,10 @@
|
||||||
"nedb": "^1.8.0",
|
"nedb": "^1.8.0",
|
||||||
"nodemailer": "^2.7.0",
|
"nodemailer": "^2.7.0",
|
||||||
"object-path": "^0.11.3",
|
"object-path": "^0.11.3",
|
||||||
|
"pug": "^2.0.0-rc.2",
|
||||||
"randomstring": "^1.1.5",
|
"randomstring": "^1.1.5",
|
||||||
"speakeasy": "^2.0.0",
|
"speakeasy": "^2.0.0",
|
||||||
|
"u2f": "^0.1.2",
|
||||||
"winston": "^2.3.1",
|
"winston": "^2.3.1",
|
||||||
"yamljs": "^0.2.8"
|
"yamljs": "^0.2.8"
|
||||||
},
|
},
|
||||||
|
@ -52,6 +52,8 @@
|
||||||
"@types/ejs": "^2.3.33",
|
"@types/ejs": "^2.3.33",
|
||||||
"@types/express": "^4.0.35",
|
"@types/express": "^4.0.35",
|
||||||
"@types/express-session": "0.0.32",
|
"@types/express-session": "0.0.32",
|
||||||
|
"@types/jquery": "^2.0.45",
|
||||||
|
"@types/jsdom": "^2.0.30",
|
||||||
"@types/ldapjs": "^1.0.0",
|
"@types/ldapjs": "^1.0.0",
|
||||||
"@types/mocha": "^2.2.41",
|
"@types/mocha": "^2.2.41",
|
||||||
"@types/mockdate": "^2.0.0",
|
"@types/mockdate": "^2.0.0",
|
||||||
|
@ -59,6 +61,7 @@
|
||||||
"@types/nodemailer": "^1.3.32",
|
"@types/nodemailer": "^1.3.32",
|
||||||
"@types/object-path": "^0.9.28",
|
"@types/object-path": "^0.9.28",
|
||||||
"@types/proxyquire": "^1.3.27",
|
"@types/proxyquire": "^1.3.27",
|
||||||
|
"@types/query-string": "^4.3.1",
|
||||||
"@types/randomstring": "^1.1.5",
|
"@types/randomstring": "^1.1.5",
|
||||||
"@types/request": "0.0.43",
|
"@types/request": "0.0.43",
|
||||||
"@types/sinon": "^2.2.1",
|
"@types/sinon": "^2.2.1",
|
||||||
|
@ -66,12 +69,25 @@
|
||||||
"@types/tmp": "0.0.33",
|
"@types/tmp": "0.0.33",
|
||||||
"@types/winston": "^2.3.2",
|
"@types/winston": "^2.3.2",
|
||||||
"@types/yamljs": "^0.2.30",
|
"@types/yamljs": "^0.2.30",
|
||||||
|
"apidoc": "^0.17.6",
|
||||||
|
"browserify": "^14.3.0",
|
||||||
"grunt": "^1.0.1",
|
"grunt": "^1.0.1",
|
||||||
|
"grunt-browserify": "^5.0.0",
|
||||||
|
"grunt-contrib-concat": "^1.0.1",
|
||||||
"grunt-contrib-copy": "^1.0.0",
|
"grunt-contrib-copy": "^1.0.0",
|
||||||
|
"grunt-contrib-cssmin": "^2.2.0",
|
||||||
|
"grunt-contrib-watch": "^1.0.0",
|
||||||
"grunt-run": "^0.6.0",
|
"grunt-run": "^0.6.0",
|
||||||
|
"istanbul": "^0.4.5",
|
||||||
|
"jquery": "^3.2.1",
|
||||||
|
"js-logger": "^1.3.0",
|
||||||
|
"jsdom": "^11.0.0",
|
||||||
"mocha": "^3.2.0",
|
"mocha": "^3.2.0",
|
||||||
"mockdate": "^2.0.1",
|
"mockdate": "^2.0.1",
|
||||||
|
"notifyjs-browser": "^0.4.2",
|
||||||
|
"nyc": "^10.3.2",
|
||||||
"proxyquire": "^1.8.0",
|
"proxyquire": "^1.8.0",
|
||||||
|
"query-string": "^4.3.4",
|
||||||
"request": "^2.79.0",
|
"request": "^2.79.0",
|
||||||
"should": "^11.1.1",
|
"should": "^11.1.1",
|
||||||
"sinon": "^1.17.6",
|
"sinon": "^1.17.6",
|
||||||
|
@ -79,6 +95,31 @@
|
||||||
"tmp": "0.0.31",
|
"tmp": "0.0.31",
|
||||||
"ts-node": "^3.0.4",
|
"ts-node": "^3.0.4",
|
||||||
"tslint": "^5.2.0",
|
"tslint": "^5.2.0",
|
||||||
"typescript": "^2.3.2"
|
"typescript": "^2.3.2",
|
||||||
|
"u2f-api": "0.0.9",
|
||||||
|
"uglify-es": "^3.0.15"
|
||||||
|
},
|
||||||
|
"nyc": {
|
||||||
|
"include": [
|
||||||
|
"src/*.ts",
|
||||||
|
"src/**/*.ts"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"doc",
|
||||||
|
"src/types",
|
||||||
|
"dist",
|
||||||
|
"test"
|
||||||
|
],
|
||||||
|
"extension": [
|
||||||
|
".ts"
|
||||||
|
],
|
||||||
|
"require": [
|
||||||
|
"ts-node/register"
|
||||||
|
],
|
||||||
|
"reporter": [
|
||||||
|
"json",
|
||||||
|
"html"
|
||||||
|
],
|
||||||
|
"all": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
6
src/client/css/00-bootstrap.min.css
vendored
Normal file
4
src/client/css/01-main.css
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI1NiIgaGVpZ2h0PSIxMDAiPgo8cmVjdCB3aWR0aD0iNTYiIGhlaWdodD0iMTAwIiBmaWxsPSIjRkZGRkZGIj48L3JlY3Q+CjxwYXRoIGQ9Ik0yOCA2NkwwIDUwTDAgMTZMMjggMEw1NiAxNkw1NiA1MEwyOCA2NkwyOCAxMDAiIGZpbGw9Im5vbmUiIHN0cm9rZT0iI0ZDRkNGQyIgc3Ryb2tlLXdpZHRoPSIyIj48L3BhdGg+CjxwYXRoIGQ9Ik0yOCAwTDI4IDM0TDAgNTBMMCA4NEwyOCAxMDBMNTYgODRMNTYgNTBMMjggMzQiIGZpbGw9Im5vbmUiIHN0cm9rZT0iI0ZCRkJGQiIgc3Ryb2tlLXdpZHRoPSIyIj48L3BhdGg+Cjwvc3ZnPg==");
|
||||||
|
}
|
101
src/client/css/02-login.css
Normal file
|
@ -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);
|
||||||
|
}
|
12
src/client/css/03-errors.css
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
|
||||||
|
.error-401 .header-img {
|
||||||
|
border-radius: 0%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-403 .header-img {
|
||||||
|
border-radius: 0%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-404 .header-img {
|
||||||
|
border-radius: 0%;
|
||||||
|
}
|
4
src/client/css/03-password-reset-form.css
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
|
||||||
|
.password-reset-form .header-img {
|
||||||
|
border-radius: 0%;
|
||||||
|
}
|
4
src/client/css/03-password-reset-request.css
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
|
||||||
|
.password-reset-request .header-img {
|
||||||
|
border-radius: 0%;
|
||||||
|
}
|
12
src/client/css/03-totp-register.css
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
.totp-register #secret {
|
||||||
|
background-color: white;
|
||||||
|
font-size: 0.9em;
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 5px;
|
||||||
|
border: 1px solid #c7c7c7;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.totp-register #qrcode img {
|
||||||
|
margin: 20px auto;
|
||||||
|
}
|
5
src/client/css/03-u2f-register.css
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
|
||||||
|
.u2f-register img {
|
||||||
|
display: block;
|
||||||
|
margin: 20px auto;
|
||||||
|
}
|
20
src/client/firstfactor/FirstFactorValidator.ts
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
|
||||||
|
import BluebirdPromise = require("bluebird");
|
||||||
|
import Endpoints = require("../../server/endpoints");
|
||||||
|
|
||||||
|
export function validate(username: string, password: string, $: JQueryStatic): BluebirdPromise < void> {
|
||||||
|
return new BluebirdPromise<void>(function (resolve, reject) {
|
||||||
|
$.post(Endpoints.FIRST_FACTOR_POST, {
|
||||||
|
username: username,
|
||||||
|
password: password,
|
||||||
|
})
|
||||||
|
.done(function () {
|
||||||
|
resolve();
|
||||||
|
})
|
||||||
|
.fail(function (xhr: JQueryXHR, textStatus: string) {
|
||||||
|
if (xhr.status == 401)
|
||||||
|
reject(new Error("Authetication failed. Please check your credentials"));
|
||||||
|
reject(new Error(textStatus));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
3
src/client/firstfactor/UISelectors.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
|
||||||
|
export const USERNAME_FIELD_ID = "#username";
|
||||||
|
export const PASSWORD_FIELD_ID = "#password";
|
39
src/client/firstfactor/index.ts
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import FirstFactorValidator = require("./FirstFactorValidator");
|
||||||
|
import JSLogger = require("js-logger");
|
||||||
|
import UISelectors = require("./UISelectors");
|
||||||
|
|
||||||
|
import Endpoints = require("../../server/endpoints");
|
||||||
|
|
||||||
|
export default function (window: Window, $: JQueryStatic, firstFactorValidator: typeof FirstFactorValidator, jslogger: typeof JSLogger) {
|
||||||
|
function onFormSubmitted() {
|
||||||
|
const username: string = $(UISelectors.USERNAME_FIELD_ID).val();
|
||||||
|
const password: string = $(UISelectors.PASSWORD_FIELD_ID).val();
|
||||||
|
jslogger.debug("Form submitted");
|
||||||
|
firstFactorValidator.validate(username, password, $)
|
||||||
|
.then(onFirstFactorSuccess, onFirstFactorFailure);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onFirstFactorSuccess() {
|
||||||
|
jslogger.debug("First factor validated.");
|
||||||
|
$(UISelectors.USERNAME_FIELD_ID).val("");
|
||||||
|
$(UISelectors.PASSWORD_FIELD_ID).val("");
|
||||||
|
|
||||||
|
// Redirect to second factor
|
||||||
|
window.location.href = Endpoints.SECOND_FACTOR_GET;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onFirstFactorFailure(err: Error) {
|
||||||
|
jslogger.debug("First factor failed.");
|
||||||
|
|
||||||
|
$(UISelectors.PASSWORD_FIELD_ID).val("");
|
||||||
|
$.notify("Error during authentication: " + err.message, "error");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$(window.document).ready(function () {
|
||||||
|
jslogger.info("Enter first factor");
|
||||||
|
$("form").on("submit", onFormSubmitted);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
BIN
src/client/img/icon.png
Normal file
After Width: | Height: | Size: 814 B |
BIN
src/client/img/mail.png
Normal file
After Width: | Height: | Size: 3.5 KiB |
BIN
src/client/img/padlock.png
Normal file
After Width: | Height: | Size: 3.2 KiB |
BIN
src/client/img/password.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 6.6 KiB |
BIN
src/client/img/success.png
Normal file
After Width: | Height: | Size: 3.1 KiB |
BIN
src/client/img/user.png
Normal file
After Width: | Height: | Size: 2.9 KiB |
BIN
src/client/img/warning.png
Normal file
After Width: | Height: | Size: 3.9 KiB |
38
src/client/index.ts
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
|
||||||
|
import FirstFactorValidator = require("./firstfactor/FirstFactorValidator");
|
||||||
|
|
||||||
|
import FirstFactor from "./firstfactor/index";
|
||||||
|
import SecondFactor from "./secondfactor/index";
|
||||||
|
import TOTPRegister from "./totp-register/totp-register";
|
||||||
|
import U2fRegister from "./u2f-register/u2f-register";
|
||||||
|
import ResetPasswordRequest from "./reset-password/reset-password-request";
|
||||||
|
import ResetPasswordForm from "./reset-password/reset-password-form";
|
||||||
|
import jslogger = require("js-logger");
|
||||||
|
import jQuery = require("jquery");
|
||||||
|
import u2fApi = require("u2f-api");
|
||||||
|
|
||||||
|
jslogger.useDefaults();
|
||||||
|
jslogger.setLevel(jslogger.INFO);
|
||||||
|
|
||||||
|
require("notifyjs-browser")(jQuery);
|
||||||
|
|
||||||
|
export = {
|
||||||
|
firstfactor: function () {
|
||||||
|
FirstFactor(window, jQuery, FirstFactorValidator, jslogger);
|
||||||
|
},
|
||||||
|
secondfactor: function () {
|
||||||
|
SecondFactor(window, jQuery, u2fApi);
|
||||||
|
},
|
||||||
|
register_totp: function() {
|
||||||
|
TOTPRegister(window, jQuery);
|
||||||
|
},
|
||||||
|
register_u2f: function () {
|
||||||
|
U2fRegister(window, jQuery);
|
||||||
|
},
|
||||||
|
reset_password_request: function () {
|
||||||
|
ResetPasswordRequest(window, jQuery);
|
||||||
|
},
|
||||||
|
reset_password_form: function () {
|
||||||
|
ResetPasswordForm(window, jQuery);
|
||||||
|
}
|
||||||
|
};
|
2
src/client/reset-password/constants.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
|
||||||
|
export const FORM_SELECTOR = ".form-signin";
|
49
src/client/reset-password/reset-password-form.ts
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
import BluebirdPromise = require("bluebird");
|
||||||
|
|
||||||
|
import Endpoints = require("../../server/endpoints");
|
||||||
|
import Constants = require("./constants");
|
||||||
|
|
||||||
|
export default function (window: Window, $: JQueryStatic) {
|
||||||
|
function modifyPassword(newPassword: string) {
|
||||||
|
return new BluebirdPromise(function (resolve, reject) {
|
||||||
|
$.post(Endpoints.RESET_PASSWORD_FORM_POST, {
|
||||||
|
password: newPassword,
|
||||||
|
})
|
||||||
|
.done(function (data) {
|
||||||
|
resolve(data);
|
||||||
|
})
|
||||||
|
.fail(function (xhr, status) {
|
||||||
|
reject(status);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onFormSubmitted() {
|
||||||
|
const password1 = $("#password1").val();
|
||||||
|
const password2 = $("#password2").val();
|
||||||
|
|
||||||
|
if (!password1 || !password2) {
|
||||||
|
$.notify("You must enter your new password twice.", "warn");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password1 != password2) {
|
||||||
|
$.notify("The passwords are different", "warn");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
modifyPassword(password1)
|
||||||
|
.then(function () {
|
||||||
|
$.notify("Your password has been changed. Please login again", "success");
|
||||||
|
window.location.href = Endpoints.FIRST_FACTOR_GET;
|
||||||
|
})
|
||||||
|
.error(function () {
|
||||||
|
$.notify("An error occurred during password change.", "warn");
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$(document).ready(function () {
|
||||||
|
$(Constants.FORM_SELECTOR).on("submit", onFormSubmitted);
|
||||||
|
});
|
||||||
|
}
|
49
src/client/reset-password/reset-password-request.ts
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
|
||||||
|
import BluebirdPromise = require("bluebird");
|
||||||
|
|
||||||
|
import Endpoints = require("../../server/endpoints");
|
||||||
|
import Constants = require("./constants");
|
||||||
|
import jslogger = require("js-logger");
|
||||||
|
|
||||||
|
export default function(window: Window, $: JQueryStatic) {
|
||||||
|
function requestPasswordReset(username: string) {
|
||||||
|
return new BluebirdPromise(function (resolve, reject) {
|
||||||
|
$.get(Endpoints.RESET_PASSWORD_IDENTITY_START_GET, {
|
||||||
|
userid: username,
|
||||||
|
})
|
||||||
|
.done(function () {
|
||||||
|
resolve();
|
||||||
|
})
|
||||||
|
.fail(function (xhr: JQueryXHR, textStatus: string) {
|
||||||
|
reject(new Error(textStatus));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onFormSubmitted() {
|
||||||
|
const username = $("#username").val();
|
||||||
|
|
||||||
|
if (!username) {
|
||||||
|
$.notify("You must provide your username to reset your password.", "warn");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
requestPasswordReset(username)
|
||||||
|
.then(function () {
|
||||||
|
$.notify("An email has been sent. Click on the link to change your password", "success");
|
||||||
|
setTimeout(function () {
|
||||||
|
window.location.replace(Endpoints.FIRST_FACTOR_GET);
|
||||||
|
}, 1000);
|
||||||
|
})
|
||||||
|
.error(function () {
|
||||||
|
$.notify("Are you sure this is your username?", "warn");
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$(document).ready(function () {
|
||||||
|
jslogger.debug("Reset password request form setup");
|
||||||
|
$(Constants.FORM_SELECTOR).on("submit", onFormSubmitted);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
22
src/client/secondfactor/TOTPValidator.ts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
|
||||||
|
import BluebirdPromise = require("bluebird");
|
||||||
|
import Endpoints = require("../../server/endpoints");
|
||||||
|
|
||||||
|
export function validate(token: string, $: JQueryStatic): BluebirdPromise<string> {
|
||||||
|
return new BluebirdPromise<string>(function (resolve, reject) {
|
||||||
|
$.ajax({
|
||||||
|
url: Endpoints.SECOND_FACTOR_TOTP_POST,
|
||||||
|
data: {
|
||||||
|
token: token,
|
||||||
|
},
|
||||||
|
method: "POST",
|
||||||
|
dataType: "json"
|
||||||
|
} as JQueryAjaxSettings)
|
||||||
|
.done(function (data: any) {
|
||||||
|
resolve(data);
|
||||||
|
})
|
||||||
|
.fail(function (xhr: JQueryXHR, textStatus: string) {
|
||||||
|
reject(new Error(textStatus));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
61
src/client/secondfactor/U2FValidator.ts
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
|
||||||
|
import U2fApi = require("u2f-api");
|
||||||
|
import U2f = require("u2f");
|
||||||
|
import BluebirdPromise = require("bluebird");
|
||||||
|
import { SignMessage } from "../../server/lib/routes/secondfactor/u2f/sign_request/SignMessage";
|
||||||
|
import Endpoints = require("../../server/endpoints");
|
||||||
|
|
||||||
|
function finishU2fAuthentication(responseData: U2fApi.SignResponse, $: JQueryStatic): BluebirdPromise<void> {
|
||||||
|
return new BluebirdPromise<void>(function (resolve, reject) {
|
||||||
|
$.ajax({
|
||||||
|
url: Endpoints.SECOND_FACTOR_U2F_SIGN_POST,
|
||||||
|
data: responseData,
|
||||||
|
method: "POST",
|
||||||
|
dataType: "json"
|
||||||
|
} as JQueryAjaxSettings)
|
||||||
|
.done(function (data) {
|
||||||
|
resolve(data);
|
||||||
|
})
|
||||||
|
.fail(function (xhr: JQueryXHR, textStatus: string) {
|
||||||
|
reject(new Error(textStatus));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function startU2fAuthentication($: JQueryStatic, u2fApi: typeof U2fApi): BluebirdPromise<void> {
|
||||||
|
return new BluebirdPromise<void>(function (resolve, reject) {
|
||||||
|
$.get(Endpoints.SECOND_FACTOR_U2F_SIGN_REQUEST_GET, {}, undefined, "json")
|
||||||
|
.done(function (signResponse: SignMessage) {
|
||||||
|
$.notify("Please touch the token", "info");
|
||||||
|
|
||||||
|
const signRequest: U2fApi.SignRequest = {
|
||||||
|
appId: signResponse.request.appId,
|
||||||
|
challenge: signResponse.request.challenge,
|
||||||
|
keyHandle: signResponse.keyHandle, // linked to the client session cookie
|
||||||
|
version: "U2F_V2"
|
||||||
|
};
|
||||||
|
|
||||||
|
u2fApi.sign([signRequest], 60)
|
||||||
|
.then(function (signResponse: U2fApi.SignResponse) {
|
||||||
|
finishU2fAuthentication(signResponse, $)
|
||||||
|
.then(function (data) {
|
||||||
|
resolve(data);
|
||||||
|
}, function (err) {
|
||||||
|
$.notify("Error when finish U2F transaction", "error");
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(function (err: Error) {
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.fail(function (xhr: JQueryXHR, textStatus: string) {
|
||||||
|
reject(new Error(textStatus));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function validate($: JQueryStatic, u2fApi: typeof U2fApi): BluebirdPromise<void> {
|
||||||
|
return startU2fAuthentication($, u2fApi);
|
||||||
|
}
|
5
src/client/secondfactor/constants.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
|
||||||
|
export const TOTP_FORM_SELECTOR = ".form-signin.totp";
|
||||||
|
export const TOTP_TOKEN_SELECTOR = ".form-signin #token";
|
||||||
|
|
||||||
|
export const U2F_FORM_SELECTOR = ".form-signin.u2f";
|
57
src/client/secondfactor/index.ts
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
|
||||||
|
import U2fApi = require("u2f-api");
|
||||||
|
import jslogger = require("js-logger");
|
||||||
|
|
||||||
|
import TOTPValidator = require("./TOTPValidator");
|
||||||
|
import U2FValidator = require("./U2FValidator");
|
||||||
|
|
||||||
|
import Endpoints = require("../../server/endpoints");
|
||||||
|
|
||||||
|
import Constants = require("./constants");
|
||||||
|
|
||||||
|
|
||||||
|
export default function (window: Window, $: JQueryStatic, u2fApi: typeof U2fApi) {
|
||||||
|
function onAuthenticationSuccess(data: any) {
|
||||||
|
window.location.href = data.redirection_url;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function onSecondFactorTotpSuccess(data: any) {
|
||||||
|
onAuthenticationSuccess(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSecondFactorTotpFailure(err: Error) {
|
||||||
|
$.notify("Error while validating TOTP token. Cause: " + err.message, "error");
|
||||||
|
}
|
||||||
|
|
||||||
|
function onU2fAuthenticationSuccess(data: any) {
|
||||||
|
onAuthenticationSuccess(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onU2fAuthenticationFailure() {
|
||||||
|
$.notify("Problem with U2F authentication. Did you register before authenticating?", "warn");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function onTOTPFormSubmitted(): boolean {
|
||||||
|
const token = $(Constants.TOTP_TOKEN_SELECTOR).val();
|
||||||
|
jslogger.debug("TOTP token is %s", token);
|
||||||
|
|
||||||
|
TOTPValidator.validate(token, $)
|
||||||
|
.then(onSecondFactorTotpSuccess)
|
||||||
|
.catch(onSecondFactorTotpFailure);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onU2FFormSubmitted(): boolean {
|
||||||
|
jslogger.debug("Start U2F authentication");
|
||||||
|
U2FValidator.validate($, U2fApi)
|
||||||
|
.then(onU2fAuthenticationSuccess, onU2fAuthenticationFailure);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$(window.document).ready(function () {
|
||||||
|
$(Constants.TOTP_FORM_SELECTOR).on("submit", onTOTPFormSubmitted);
|
||||||
|
$(Constants.U2F_FORM_SELECTOR).on("submit", onU2FFormSubmitted);
|
||||||
|
});
|
||||||
|
}
|
11
src/client/totp-register/totp-register.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
|
||||||
|
import jslogger = require("js-logger");
|
||||||
|
import UISelector = require("./ui-selector");
|
||||||
|
|
||||||
|
export default function(window: Window, $: JQueryStatic) {
|
||||||
|
jslogger.debug("Creating QRCode from OTPAuth url");
|
||||||
|
const qrcode = $(UISelector.QRCODE_ID_SELECTOR);
|
||||||
|
const val = qrcode.text();
|
||||||
|
qrcode.empty();
|
||||||
|
new (window as any).QRCode(qrcode.get(0), val);
|
||||||
|
}
|
2
src/client/totp-register/ui-selector.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
|
||||||
|
export const QRCODE_ID_SELECTOR = "#qrcode";
|
53
src/client/u2f-register/u2f-register.ts
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
|
||||||
|
import BluebirdPromise = require("bluebird");
|
||||||
|
import U2f = require("u2f");
|
||||||
|
import u2fApi = require("u2f-api");
|
||||||
|
|
||||||
|
import Endpoints = require("../../server/endpoints");
|
||||||
|
import jslogger = require("js-logger");
|
||||||
|
|
||||||
|
export default function(window: Window, $: JQueryStatic) {
|
||||||
|
|
||||||
|
function checkRegistration(regResponse: u2fApi.RegisterResponse, fn: (err: Error) => void) {
|
||||||
|
const registrationData: U2f.RegistrationData = regResponse;
|
||||||
|
|
||||||
|
jslogger.debug("registrationResponse = %s", JSON.stringify(registrationData));
|
||||||
|
|
||||||
|
$.post(Endpoints.SECOND_FACTOR_U2F_REGISTER_POST, registrationData, undefined, "json")
|
||||||
|
.done(function (data) {
|
||||||
|
document.location.href = data.redirection_url;
|
||||||
|
})
|
||||||
|
.fail(function (xhr, status) {
|
||||||
|
$.notify("Error when finish U2F transaction" + status);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function requestRegistration(fn: (err: Error) => void) {
|
||||||
|
$.get(Endpoints.SECOND_FACTOR_U2F_REGISTER_REQUEST_GET, {}, undefined, "json")
|
||||||
|
.done(function (registrationRequest: U2f.Request) {
|
||||||
|
jslogger.debug("registrationRequest = %s", JSON.stringify(registrationRequest));
|
||||||
|
|
||||||
|
const registerRequest: u2fApi.RegisterRequest = registrationRequest;
|
||||||
|
u2fApi.register([registerRequest], [], 120)
|
||||||
|
.then(function (res: u2fApi.RegisterResponse) {
|
||||||
|
checkRegistration(res, fn);
|
||||||
|
})
|
||||||
|
.catch(function (err: Error) {
|
||||||
|
fn(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onRegisterFailure(err: Error) {
|
||||||
|
$.notify("Problem authenticating with U2F.", "error");
|
||||||
|
}
|
||||||
|
|
||||||
|
$(document).ready(function () {
|
||||||
|
requestRegistration(function (err: Error) {
|
||||||
|
if (err) {
|
||||||
|
onRegisterFailure(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
|
@ -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;
|
|
||||||
}
|
|
4
src/public_html/js/jquery.min.js
vendored
|
@ -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
src/public_html/js/notify.min.js
vendored
|
@ -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);
|
|
||||||
});
|
|
||||||
|
|
||||||
})();
|
|
296
src/server/endpoints.ts
Normal file
|
@ -0,0 +1,296 @@
|
||||||
|
/**
|
||||||
|
* @apiDefine UserSession
|
||||||
|
* @apiHeader {String} Cookie Cookie containing "connect.sid", the user
|
||||||
|
* session token.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @apiDefine InternalError
|
||||||
|
* @apiError (Error 500) {String} error Internal error message.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @apiDefine IdentityValidationStart
|
||||||
|
*
|
||||||
|
* @apiSuccess (Success 204) status Identity validation has been initiated.
|
||||||
|
* @apiError (Error 403) AccessDenied Access is denied.
|
||||||
|
* @apiError (Error 400) InvalidIdentity User identity is invalid.
|
||||||
|
* @apiError (Error 500) {String} error Internal error message.
|
||||||
|
*
|
||||||
|
* @apiDescription This request issue an identity validation token for the user
|
||||||
|
* bound to the session. It sends a challenge to the email address set in the user
|
||||||
|
* LDAP entry. The user must visit the sent URL to complete the validation and
|
||||||
|
* continue the registration process.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @apiDefine IdentityValidationFinish
|
||||||
|
* @apiParam {String} identity_token The one-time identity validation token provided in the email.
|
||||||
|
* @apiSuccess (Success 200) {String} content The content of the page.
|
||||||
|
* @apiError (Error 403) AccessDenied Access is denied.
|
||||||
|
* @apiError (Error 500) {String} error Internal error message.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {post} /api/secondfactor/u2f/register Complete U2F registration
|
||||||
|
* @apiName FinishU2FRegistration
|
||||||
|
* @apiGroup U2F
|
||||||
|
* @apiVersion 1.0.0
|
||||||
|
* @apiUse UserSession
|
||||||
|
* @apiUse InternalError
|
||||||
|
*
|
||||||
|
* @apiSuccess (Success 302) Redirect to the URL that has been stored during last call to /verify.
|
||||||
|
*
|
||||||
|
* @apiDescription Complete U2F registration request.
|
||||||
|
*/
|
||||||
|
export const SECOND_FACTOR_U2F_REGISTER_POST = "/api/u2f/register";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {get} /api/u2f/register_request Start U2F registration
|
||||||
|
* @apiName StartU2FRegistration
|
||||||
|
* @apiGroup U2F
|
||||||
|
* @apiVersion 1.0.0
|
||||||
|
* @apiUse UserSession
|
||||||
|
* @apiUse InternalError
|
||||||
|
*
|
||||||
|
* @apiSuccess (Success 200) authentication_request The U2F registration request.
|
||||||
|
* @apiError (Error 403) {none} error Unexpected identity validation challenge.
|
||||||
|
*
|
||||||
|
* @apiDescription Initiate a U2F device registration request.
|
||||||
|
*/
|
||||||
|
export const SECOND_FACTOR_U2F_REGISTER_REQUEST_GET = "/api/u2f/register_request";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {post} /api/u2f/sign Complete U2F authentication
|
||||||
|
* @apiName CompleteU2FAuthentication
|
||||||
|
* @apiGroup U2F
|
||||||
|
* @apiVersion 1.0.0
|
||||||
|
* @apiUse UserSession
|
||||||
|
* @apiUse InternalError
|
||||||
|
*
|
||||||
|
* @apiSuccess (Success 302) Redirect to the URL that has been stored during last call to /verify.
|
||||||
|
* @apiError (Error 403) {none} error No authentication request has been provided.
|
||||||
|
*
|
||||||
|
* @apiDescription Complete authentication request of the U2F device.
|
||||||
|
*/
|
||||||
|
export const SECOND_FACTOR_U2F_SIGN_POST = "/api/u2f/sign";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {get} /api/u2f/sign_request Start U2F authentication
|
||||||
|
* @apiName StartU2FAuthentication
|
||||||
|
* @apiGroup U2F
|
||||||
|
* @apiVersion 1.0.0
|
||||||
|
* @apiUse UserSession
|
||||||
|
* @apiUse InternalError
|
||||||
|
*
|
||||||
|
* @apiSuccess (Success 200) authentication_request The U2F authentication request.
|
||||||
|
* @apiError (Error 401) {none} error There is no key registered for user in session.
|
||||||
|
*
|
||||||
|
* @apiDescription Initiate an authentication request using a U2F device.
|
||||||
|
*/
|
||||||
|
export const SECOND_FACTOR_U2F_SIGN_REQUEST_GET = "/api/u2f/sign_request";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {post} /api/totp Complete TOTP authentication
|
||||||
|
* @apiName ValidateTOTPSecondFactor
|
||||||
|
* @apiGroup TOTP
|
||||||
|
* @apiVersion 1.0.0
|
||||||
|
* @apiUse UserSession
|
||||||
|
* @apiUse InternalError
|
||||||
|
*
|
||||||
|
* @apiParam {String} token TOTP token.
|
||||||
|
*
|
||||||
|
* @apiSuccess (Success 302) Redirect to the URL that has been stored during last call to /verify.
|
||||||
|
* @apiError (Error 401) {none} error TOTP token is invalid.
|
||||||
|
*
|
||||||
|
* @apiDescription Verify TOTP token. The user is authenticated upon success.
|
||||||
|
*/
|
||||||
|
export const SECOND_FACTOR_TOTP_POST = "/api/totp";
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {get} /secondfactor/u2f/identity/start Start U2F registration identity validation
|
||||||
|
* @apiName RequestU2FRegistration
|
||||||
|
* @apiGroup U2F
|
||||||
|
* @apiVersion 1.0.0
|
||||||
|
* @apiUse UserSession
|
||||||
|
* @apiUse IdentityValidationStart
|
||||||
|
*/
|
||||||
|
export const SECOND_FACTOR_U2F_IDENTITY_START_GET = "/secondfactor/u2f/identity/start";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {get} /secondfactor/u2f/identity/finish Finish U2F registration identity validation
|
||||||
|
* @apiName ServeU2FRegistrationPage
|
||||||
|
* @apiGroup U2F
|
||||||
|
* @apiVersion 1.0.0
|
||||||
|
* @apiUse UserSession
|
||||||
|
* @apiUse IdentityValidationFinish
|
||||||
|
*
|
||||||
|
* @apiDescription Serves the U2F registration page that asks the user to
|
||||||
|
* touch the token of the U2F device.
|
||||||
|
*/
|
||||||
|
export const SECOND_FACTOR_U2F_IDENTITY_FINISH_GET = "/secondfactor/u2f/identity/finish";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {get} /secondfactor/totp/identity/start Start TOTP registration identity validation
|
||||||
|
* @apiName StartTOTPRegistration
|
||||||
|
* @apiGroup TOTP
|
||||||
|
* @apiVersion 1.0.0
|
||||||
|
* @apiUse UserSession
|
||||||
|
* @apiUse IdentityValidationStart
|
||||||
|
*
|
||||||
|
* @apiDescription Initiates the identity validation
|
||||||
|
*/
|
||||||
|
export const SECOND_FACTOR_TOTP_IDENTITY_START_GET = "/secondfactor/totp/identity/start";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {get} /secondfactor/totp/identity/finish Finish TOTP registration identity validation
|
||||||
|
* @apiName FinishTOTPRegistration
|
||||||
|
* @apiGroup TOTP
|
||||||
|
* @apiVersion 1.0.0
|
||||||
|
* @apiUse UserSession
|
||||||
|
* @apiUse IdentityValidationFinish
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @apiDescription Serves the TOTP registration page that displays the secret.
|
||||||
|
* The secret is a QRCode and a base32 secret.
|
||||||
|
*/
|
||||||
|
export const SECOND_FACTOR_TOTP_IDENTITY_FINISH_GET = "/secondfactor/totp/identity/finish";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {post} /api/password-reset Set new password
|
||||||
|
* @apiName SetNewLDAPPassword
|
||||||
|
* @apiGroup PasswordReset
|
||||||
|
* @apiVersion 1.0.0
|
||||||
|
* @apiUse UserSession
|
||||||
|
*
|
||||||
|
* @apiParam {String} password New password
|
||||||
|
*
|
||||||
|
* @apiDescription Set a new password for the user.
|
||||||
|
*/
|
||||||
|
export const RESET_PASSWORD_FORM_POST = "/api/password-reset";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {get} /password-reset/request Request username
|
||||||
|
* @apiName ServePasswordResetPage
|
||||||
|
* @apiGroup PasswordReset
|
||||||
|
* @apiVersion 1.0.0
|
||||||
|
* @apiUse UserSession
|
||||||
|
*
|
||||||
|
* @apiDescription Serve a page that requires the username.
|
||||||
|
*/
|
||||||
|
export const RESET_PASSWORD_REQUEST_GET = "/password-reset/request";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {get} /password-reset/identity/start Start password reset request
|
||||||
|
* @apiName StartPasswordResetRequest
|
||||||
|
* @apiGroup PasswordReset
|
||||||
|
* @apiVersion 1.0.0
|
||||||
|
* @apiUse UserSession
|
||||||
|
* @apiUse IdentityValidationStart
|
||||||
|
*
|
||||||
|
* @apiDescription Start password reset request.
|
||||||
|
*/
|
||||||
|
export const RESET_PASSWORD_IDENTITY_START_GET = "/password-reset/identity/start";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {post} /reset-password/request Finish password reset request
|
||||||
|
* @apiName FinishPasswordResetRequest
|
||||||
|
* @apiGroup PasswordReset
|
||||||
|
* @apiVersion 1.0.0
|
||||||
|
* @apiUse UserSession
|
||||||
|
* @apiUse IdentityValidationFinish
|
||||||
|
*
|
||||||
|
* @apiDescription Start password reset request.
|
||||||
|
*/
|
||||||
|
export const RESET_PASSWORD_IDENTITY_FINISH_GET = "/password-reset/identity/finish";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {post} /1stfactor Bind user against LDAP
|
||||||
|
* @apiName ValidateFirstFactor
|
||||||
|
* @apiGroup Authentication
|
||||||
|
* @apiVersion 1.0.0
|
||||||
|
* @apiUse UserSession
|
||||||
|
* @apiUse InternalError
|
||||||
|
*
|
||||||
|
* @apiParam {String} username User username.
|
||||||
|
* @apiParam {String} password User password.
|
||||||
|
*
|
||||||
|
* @apiSuccess (Success 204) status 1st factor is validated.
|
||||||
|
* @apiError (Error 401) {none} error 1st factor is not validated.
|
||||||
|
* @apiError (Error 401) {none} error Access has been restricted after too
|
||||||
|
* many authentication attempts
|
||||||
|
*
|
||||||
|
* @apiDescription Verify credentials against the LDAP.
|
||||||
|
*/
|
||||||
|
export const FIRST_FACTOR_POST = "/api/firstfactor";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {get} / First factor page
|
||||||
|
* @apiName Login
|
||||||
|
* @apiGroup Authentication
|
||||||
|
* @apiVersion 1.0.0
|
||||||
|
*
|
||||||
|
* @apiSuccess (Success 200) {String} Content The content of the first factor page.
|
||||||
|
*
|
||||||
|
* @apiDescription Serves the login page and create a create a cookie for the client.
|
||||||
|
*/
|
||||||
|
export const FIRST_FACTOR_GET = "/";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {get} /secondfactor Second factor page
|
||||||
|
* @apiName SecondFactor
|
||||||
|
* @apiGroup Authentication
|
||||||
|
* @apiVersion 1.0.0
|
||||||
|
*
|
||||||
|
* @apiSuccess (Success 200) {String} Content The content of second factor page.
|
||||||
|
*
|
||||||
|
* @apiDescription Serves the second factor page
|
||||||
|
*/
|
||||||
|
export const SECOND_FACTOR_GET = "/secondfactor";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {get} /verify Verify user authentication
|
||||||
|
* @apiName VerifyAuthentication
|
||||||
|
* @apiGroup Verification
|
||||||
|
* @apiVersion 1.0.0
|
||||||
|
* @apiUse UserSession
|
||||||
|
*
|
||||||
|
* @apiSuccess (Success 204) status The user is authenticated.
|
||||||
|
* @apiError (Error 401) status The user is not authenticated.
|
||||||
|
*
|
||||||
|
* @apiDescription Verify that the user is authenticated, i.e., the two
|
||||||
|
* factors have been validated
|
||||||
|
*/
|
||||||
|
export const VERIFY_GET = "/verify";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {get} /logout Serves logout page
|
||||||
|
* @apiName Logout
|
||||||
|
* @apiGroup Authentication
|
||||||
|
* @apiVersion 1.0.0
|
||||||
|
*
|
||||||
|
* @apiParam {String} redirect Redirect to this URL when user is deauthenticated.
|
||||||
|
* @apiSuccess (Success 302) redirect Redirect to the URL.
|
||||||
|
*
|
||||||
|
* @apiDescription Log out the user and redirect to the URL.
|
||||||
|
*/
|
||||||
|
export const LOGOUT_GET = "/logout";
|
||||||
|
|
||||||
|
export const ERROR_401_GET = "/error/401";
|
||||||
|
export const ERROR_403_GET = "/error/403";
|
||||||
|
export const ERROR_404_GET = "/error/404";
|
|
@ -17,7 +17,7 @@ console.log("Parse configuration file: %s", config_path);
|
||||||
const yaml_config = YAML.load(config_path);
|
const yaml_config = YAML.load(config_path);
|
||||||
|
|
||||||
const deps = {
|
const deps = {
|
||||||
u2f: require("authdog"),
|
u2f: require("u2f"),
|
||||||
nodemailer: require("nodemailer"),
|
nodemailer: require("nodemailer"),
|
||||||
ldapjs: require("ldapjs"),
|
ldapjs: require("ldapjs"),
|
||||||
session: require("express-session"),
|
session: require("express-session"),
|
|
@ -9,7 +9,7 @@ interface DatedDocument {
|
||||||
date: Date;
|
date: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class AuthenticationRegulator {
|
export class AuthenticationRegulator {
|
||||||
private _user_data_store: any;
|
private _user_data_store: any;
|
||||||
private _lock_time_in_seconds: number;
|
private _lock_time_in_seconds: number;
|
||||||
|
|
40
src/server/lib/AuthenticationSession.ts
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
|
||||||
|
|
||||||
|
import express = require("express");
|
||||||
|
import U2f = require("u2f");
|
||||||
|
|
||||||
|
export interface AuthenticationSession {
|
||||||
|
userid: string;
|
||||||
|
first_factor: boolean;
|
||||||
|
second_factor: boolean;
|
||||||
|
identity_check?: {
|
||||||
|
challenge: string;
|
||||||
|
userid: string;
|
||||||
|
};
|
||||||
|
register_request?: U2f.Request;
|
||||||
|
sign_request?: U2f.Request;
|
||||||
|
email: string;
|
||||||
|
groups: string[];
|
||||||
|
redirect?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function reset(req: express.Request): void {
|
||||||
|
const authSession: AuthenticationSession = {
|
||||||
|
first_factor: false,
|
||||||
|
second_factor: false,
|
||||||
|
userid: undefined,
|
||||||
|
email: undefined,
|
||||||
|
groups: [],
|
||||||
|
register_request: undefined,
|
||||||
|
sign_request: undefined,
|
||||||
|
identity_check: undefined,
|
||||||
|
redirect: undefined
|
||||||
|
};
|
||||||
|
req.session.auth = authSession;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function get(req: express.Request): AuthenticationSession {
|
||||||
|
if (!req.session.auth)
|
||||||
|
reset(req);
|
||||||
|
return req.session.auth;
|
||||||
|
}
|
18
src/server/lib/AuthenticationValidator.ts
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
|
||||||
|
import BluebirdPromise = require("bluebird");
|
||||||
|
import express = require("express");
|
||||||
|
import objectPath = require("object-path");
|
||||||
|
|
||||||
|
import FirstFactorValidator = require("./FirstFactorValidator");
|
||||||
|
import AuthenticationSession = require("./AuthenticationSession");
|
||||||
|
|
||||||
|
export function validate(req: express.Request): BluebirdPromise<void> {
|
||||||
|
return FirstFactorValidator.validate(req)
|
||||||
|
.then(function () {
|
||||||
|
const authSession = AuthenticationSession.get(req);
|
||||||
|
if (!authSession.second_factor)
|
||||||
|
return BluebirdPromise.reject("No second factor variable");
|
||||||
|
|
||||||
|
return BluebirdPromise.resolve();
|
||||||
|
});
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
|
|
||||||
import * as ObjectPath from "object-path";
|
import * as ObjectPath from "object-path";
|
||||||
import { AppConfiguration, UserConfiguration, NotifierConfiguration, ACLConfiguration, LdapConfiguration } from "./Configuration";
|
import { AppConfiguration, UserConfiguration, NotifierConfiguration, ACLConfiguration, LdapConfiguration } from "./../../types/Configuration";
|
||||||
|
|
||||||
|
|
||||||
function get_optional<T>(config: object, path: string, default_value: T): T {
|
function get_optional<T>(config: object, path: string, default_value: T): T {
|
26
src/server/lib/ErrorReplies.ts
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import express = require("express");
|
||||||
|
import { Winston } from "winston";
|
||||||
|
|
||||||
|
function replyWithError(res: express.Response, code: number, logger: Winston) {
|
||||||
|
return function (err: Error) {
|
||||||
|
logger.error("Reply with error %d: %s", code, err);
|
||||||
|
res.status(code);
|
||||||
|
res.send();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function replyWithError400(res: express.Response, logger: Winston) {
|
||||||
|
return replyWithError(res, 400, logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function replyWithError401(res: express.Response, logger: Winston) {
|
||||||
|
return replyWithError(res, 401, logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function replyWithError403(res: express.Response, logger: Winston) {
|
||||||
|
return replyWithError(res, 403, logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function replyWithError500(res: express.Response, logger: Winston) {
|
||||||
|
return replyWithError(res, 500, logger);
|
||||||
|
}
|
|
@ -1,9 +1,9 @@
|
||||||
|
|
||||||
export class LdapSeachError extends Error {
|
export class LdapSearchError extends Error {
|
||||||
constructor(message?: string) {
|
constructor(message?: string) {
|
||||||
super(message);
|
super(message);
|
||||||
this.name = "LdapSeachError";
|
this.name = "LdapSearchError";
|
||||||
Object.setPrototypeOf(this, LdapSeachError.prototype);
|
Object.setPrototypeOf(this, LdapSearchError.prototype);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,3 +54,19 @@ export class DomainAccessDenied extends Error {
|
||||||
Object.setPrototypeOf(this, DomainAccessDenied.prototype);
|
Object.setPrototypeOf(this, DomainAccessDenied.prototype);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class FirstFactorValidationError extends Error {
|
||||||
|
constructor(message?: string) {
|
||||||
|
super(message);
|
||||||
|
this.name = "FirstFactorValidationError";
|
||||||
|
Object.setPrototypeOf(this, FirstFactorValidationError.prototype);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SecondFactorValidationError extends Error {
|
||||||
|
constructor(message?: string) {
|
||||||
|
super(message);
|
||||||
|
this.name = "SecondFactorValidationError";
|
||||||
|
Object.setPrototypeOf(this, FirstFactorValidationError.prototype);
|
||||||
|
}
|
||||||
|
}
|
14
src/server/lib/FirstFactorValidator.ts
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
|
||||||
|
import BluebirdPromise = require("bluebird");
|
||||||
|
import express = require("express");
|
||||||
|
import objectPath = require("object-path");
|
||||||
|
import Exceptions = require("./Exceptions");
|
||||||
|
import AuthenticationSession = require("./AuthenticationSession");
|
||||||
|
|
||||||
|
export function validate(req: express.Request): BluebirdPromise<void> {
|
||||||
|
const authSession = AuthenticationSession.get(req);
|
||||||
|
if (!authSession.userid || !authSession.first_factor)
|
||||||
|
return BluebirdPromise.reject(new Exceptions.FirstFactorValidationError("First factor has not been validated yet."));
|
||||||
|
|
||||||
|
return BluebirdPromise.resolve();
|
||||||
|
}
|
130
src/server/lib/IdentityCheckMiddleware.ts
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
|
||||||
|
import objectPath = require("object-path");
|
||||||
|
import randomstring = require("randomstring");
|
||||||
|
import BluebirdPromise = require("bluebird");
|
||||||
|
import util = require("util");
|
||||||
|
import Exceptions = require("./Exceptions");
|
||||||
|
import fs = require("fs");
|
||||||
|
import ejs = require("ejs");
|
||||||
|
import UserDataStore from "./UserDataStore";
|
||||||
|
import { Winston } from "../../types/Dependencies";
|
||||||
|
import express = require("express");
|
||||||
|
import ErrorReplies = require("./ErrorReplies");
|
||||||
|
import ServerVariables = require("./ServerVariables");
|
||||||
|
import AuthenticationSession = require("./AuthenticationSession");
|
||||||
|
|
||||||
|
import Identity = require("../../types/Identity");
|
||||||
|
import { IdentityValidationRequestContent } from "./UserDataStore";
|
||||||
|
|
||||||
|
const filePath = __dirname + "/../resources/email-template.ejs";
|
||||||
|
const email_template = fs.readFileSync(filePath, "utf8");
|
||||||
|
|
||||||
|
// IdentityValidator allows user to go through a identity validation process in two steps:
|
||||||
|
// - Request an operation to be performed (password reset, registration).
|
||||||
|
// - Confirm operation with email.
|
||||||
|
|
||||||
|
export interface IdentityValidable {
|
||||||
|
challenge(): string;
|
||||||
|
preValidationInit(req: express.Request): BluebirdPromise<Identity.Identity>;
|
||||||
|
postValidationInit(req: express.Request): BluebirdPromise<void>;
|
||||||
|
|
||||||
|
preValidationResponse(req: express.Request, res: express.Response): void; // Serves a page after identity check request
|
||||||
|
postValidationResponse(req: express.Request, res: express.Response): void; // Serves the page if identity validated
|
||||||
|
mailSubject(): string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function issue_token(userid: string, content: Object, userDataStore: UserDataStore, logger: Winston): BluebirdPromise<string> {
|
||||||
|
const five_minutes = 4 * 60 * 1000;
|
||||||
|
const token = randomstring.generate({ length: 64 });
|
||||||
|
const that = this;
|
||||||
|
|
||||||
|
logger.debug("identity_check: issue identity token %s for 5 minutes", token);
|
||||||
|
return userDataStore.issue_identity_check_token(userid, token, content, five_minutes)
|
||||||
|
.then(function () {
|
||||||
|
return BluebirdPromise.resolve(token);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function consume_token(token: string, userDataStore: UserDataStore, logger: Winston): BluebirdPromise<IdentityValidationRequestContent> {
|
||||||
|
logger.debug("identity_check: consume token %s", token);
|
||||||
|
return userDataStore.consume_identity_check_token(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function register(app: express.Application, pre_validation_endpoint: string, post_validation_endpoint: string, handler: IdentityValidable) {
|
||||||
|
app.get(pre_validation_endpoint, get_start_validation(handler, post_validation_endpoint));
|
||||||
|
app.get(post_validation_endpoint, get_finish_validation(handler));
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkIdentityToken(req: express.Request, identityToken: string): BluebirdPromise<void> {
|
||||||
|
if (!identityToken)
|
||||||
|
return BluebirdPromise.reject(new Exceptions.AccessDeniedError("No identity token provided"));
|
||||||
|
return BluebirdPromise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function get_finish_validation(handler: IdentityValidable): express.RequestHandler {
|
||||||
|
return function (req: express.Request, res: express.Response): BluebirdPromise<void> {
|
||||||
|
const logger = ServerVariables.getLogger(req.app);
|
||||||
|
const userDataStore = ServerVariables.getUserDataStore(req.app);
|
||||||
|
|
||||||
|
const authSession = AuthenticationSession.get(req);
|
||||||
|
const identityToken = objectPath.get<express.Request, string>(req, "query.identity_token");
|
||||||
|
logger.info("GET identity_check: identity token provided is %s", identityToken);
|
||||||
|
|
||||||
|
return checkIdentityToken(req, identityToken)
|
||||||
|
.then(function () {
|
||||||
|
return handler.postValidationInit(req);
|
||||||
|
})
|
||||||
|
.then(function () {
|
||||||
|
return consume_token(identityToken, userDataStore, logger);
|
||||||
|
})
|
||||||
|
.then(function (content: IdentityValidationRequestContent) {
|
||||||
|
authSession.identity_check = {
|
||||||
|
challenge: handler.challenge(),
|
||||||
|
userid: content.userid
|
||||||
|
};
|
||||||
|
handler.postValidationResponse(req, res);
|
||||||
|
return BluebirdPromise.resolve();
|
||||||
|
})
|
||||||
|
.catch(Exceptions.FirstFactorValidationError, ErrorReplies.replyWithError401(res, logger))
|
||||||
|
.catch(Exceptions.AccessDeniedError, ErrorReplies.replyWithError403(res, logger))
|
||||||
|
.catch(ErrorReplies.replyWithError500(res, logger));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function get_start_validation(handler: IdentityValidable, postValidationEndpoint: string): express.RequestHandler {
|
||||||
|
return function (req: express.Request, res: express.Response): BluebirdPromise<void> {
|
||||||
|
const logger = ServerVariables.getLogger(req.app);
|
||||||
|
const notifier = ServerVariables.getNotifier(req.app);
|
||||||
|
const userDataStore = ServerVariables.getUserDataStore(req.app);
|
||||||
|
let identity: Identity.Identity;
|
||||||
|
logger.info("Identity Validation: Start identity validation");
|
||||||
|
|
||||||
|
return handler.preValidationInit(req)
|
||||||
|
.then(function (id: Identity.Identity) {
|
||||||
|
logger.debug("Identity Validation: retrieved identity is %s", JSON.stringify(id));
|
||||||
|
identity = id;
|
||||||
|
const email_address = objectPath.get<Identity.Identity, string>(identity, "email");
|
||||||
|
const userid = objectPath.get<Identity.Identity, string>(identity, "userid");
|
||||||
|
|
||||||
|
if (!(email_address && userid))
|
||||||
|
return BluebirdPromise.reject(new Exceptions.IdentityError("Missing user id or email address"));
|
||||||
|
|
||||||
|
return issue_token(userid, undefined, userDataStore, logger);
|
||||||
|
})
|
||||||
|
.then(function (token: string) {
|
||||||
|
const host = req.get("Host");
|
||||||
|
const link_url = util.format("https://%s%s?identity_token=%s", host, postValidationEndpoint, token);
|
||||||
|
logger.info("POST identity_check: notification sent to user %s", identity.userid);
|
||||||
|
return notifier.notify(identity, handler.mailSubject(), link_url);
|
||||||
|
})
|
||||||
|
.then(function () {
|
||||||
|
handler.preValidationResponse(req, res);
|
||||||
|
return BluebirdPromise.resolve();
|
||||||
|
})
|
||||||
|
.catch(Exceptions.FirstFactorValidationError, ErrorReplies.replyWithError401(res, logger))
|
||||||
|
.catch(Exceptions.IdentityError, ErrorReplies.replyWithError400(res, logger))
|
||||||
|
.catch(Exceptions.AccessDeniedError, ErrorReplies.replyWithError403(res, logger))
|
||||||
|
.catch(ErrorReplies.replyWithError500(res, logger));
|
||||||
|
};
|
||||||
|
}
|
3
src/server/lib/IdentityCheckPreValidationTemplate.ts
Normal file
|
@ -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 ldapjs = require("ldapjs");
|
||||||
|
|
||||||
import { EventEmitter } from "events";
|
import { EventEmitter } from "events";
|
||||||
import { LdapConfiguration } from "./Configuration";
|
import { LdapConfiguration } from "./../../types/Configuration";
|
||||||
import { Ldapjs } from "../types/Dependencies";
|
import { Ldapjs } from "../../types/Dependencies";
|
||||||
import { ILogger } from "../types/ILogger";
|
import { Winston } from "../../types/Dependencies";
|
||||||
|
|
||||||
interface SearchEntry {
|
interface SearchEntry {
|
||||||
object: any;
|
object: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class LdapClient {
|
export class LdapClient {
|
||||||
options: LdapConfiguration;
|
private options: LdapConfiguration;
|
||||||
ldapjs: Ldapjs;
|
private ldapjs: Ldapjs;
|
||||||
logger: ILogger;
|
private logger: Winston;
|
||||||
client: ldapjs.ClientAsync;
|
private client: ldapjs.ClientAsync;
|
||||||
|
|
||||||
constructor(options: LdapConfiguration, ldapjs: Ldapjs, logger: ILogger) {
|
constructor(options: LdapConfiguration, ldapjs: Ldapjs, logger: Winston) {
|
||||||
this.options = options;
|
this.options = options;
|
||||||
this.ldapjs = ldapjs;
|
this.ldapjs = ldapjs;
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
|
@ -60,7 +60,7 @@ export class LdapClient {
|
||||||
|
|
||||||
this.logger.debug("LDAP: Bind user %s", user_dn);
|
this.logger.debug("LDAP: Bind user %s", user_dn);
|
||||||
return this.client.bindAsync(user_dn, password)
|
return this.client.bindAsync(user_dn, password)
|
||||||
.error(function (err) {
|
.error(function (err: Error) {
|
||||||
throw new exceptions.LdapBindError(err.message);
|
throw new exceptions.LdapBindError(err.message);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -75,14 +75,14 @@ export class LdapClient {
|
||||||
doc.push(entry.object);
|
doc.push(entry.object);
|
||||||
});
|
});
|
||||||
res.on("error", function (err: Error) {
|
res.on("error", function (err: Error) {
|
||||||
reject(err);
|
reject(new exceptions.LdapSearchError(err.message));
|
||||||
});
|
});
|
||||||
res.on("end", function () {
|
res.on("end", function () {
|
||||||
resolve(doc);
|
resolve(doc);
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(function (err) {
|
.catch(function (err: Error) {
|
||||||
reject(err);
|
reject(new exceptions.LdapSearchError(err.message));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
73
src/server/lib/RestApi.ts
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
|
||||||
|
import express = require("express");
|
||||||
|
import UserDataStore from "./UserDataStore";
|
||||||
|
import { Winston } from "../../types/Dependencies";
|
||||||
|
|
||||||
|
import FirstFactorGet = require("./routes/firstfactor/get");
|
||||||
|
import SecondFactorGet = require("./routes/secondfactor/get");
|
||||||
|
|
||||||
|
import FirstFactorPost = require("./routes/firstfactor/post");
|
||||||
|
import LogoutGet = require("./routes/logout/get");
|
||||||
|
import VerifyGet = require("./routes/verify/get");
|
||||||
|
import TOTPSignGet = require("./routes/secondfactor/totp/sign/post");
|
||||||
|
|
||||||
|
import IdentityCheckMiddleware = require("./IdentityCheckMiddleware");
|
||||||
|
|
||||||
|
import TOTPRegistrationIdentityHandler from "./routes/secondfactor/totp/identity/RegistrationHandler";
|
||||||
|
import U2FRegistrationIdentityHandler from "./routes/secondfactor/u2f/identity/RegistrationHandler";
|
||||||
|
import ResetPasswordIdentityHandler from "./routes/password-reset/identity/PasswordResetHandler";
|
||||||
|
|
||||||
|
import U2FSignPost = require("./routes/secondfactor/u2f/sign/post");
|
||||||
|
import U2FSignRequestGet = require("./routes/secondfactor/u2f/sign_request/get");
|
||||||
|
|
||||||
|
import U2FRegisterPost = require("./routes/secondfactor/u2f/register/post");
|
||||||
|
import U2FRegisterRequestGet = require("./routes/secondfactor/u2f/register_request/get");
|
||||||
|
|
||||||
|
|
||||||
|
import ResetPasswordFormPost = require("./routes/password-reset/form/post");
|
||||||
|
import ResetPasswordRequestPost = require("./routes/password-reset/request/get");
|
||||||
|
|
||||||
|
import Error401Get = require("./routes/error/401/get");
|
||||||
|
import Error403Get = require("./routes/error/403/get");
|
||||||
|
import Error404Get = require("./routes/error/404/get");
|
||||||
|
|
||||||
|
|
||||||
|
import Endpoints = require("../endpoints");
|
||||||
|
|
||||||
|
export default class RestApi {
|
||||||
|
static setup(app: express.Application): void {
|
||||||
|
app.get(Endpoints.FIRST_FACTOR_GET, FirstFactorGet.default);
|
||||||
|
app.get(Endpoints.SECOND_FACTOR_GET, SecondFactorGet.default);
|
||||||
|
app.get(Endpoints.LOGOUT_GET, LogoutGet.default);
|
||||||
|
|
||||||
|
IdentityCheckMiddleware.register(app, Endpoints.SECOND_FACTOR_TOTP_IDENTITY_START_GET,
|
||||||
|
Endpoints.SECOND_FACTOR_TOTP_IDENTITY_FINISH_GET, new TOTPRegistrationIdentityHandler());
|
||||||
|
|
||||||
|
IdentityCheckMiddleware.register(app, Endpoints.SECOND_FACTOR_U2F_IDENTITY_START_GET,
|
||||||
|
Endpoints.SECOND_FACTOR_U2F_IDENTITY_FINISH_GET, new U2FRegistrationIdentityHandler());
|
||||||
|
|
||||||
|
IdentityCheckMiddleware.register(app, Endpoints.RESET_PASSWORD_IDENTITY_START_GET,
|
||||||
|
Endpoints.RESET_PASSWORD_IDENTITY_FINISH_GET, new ResetPasswordIdentityHandler());
|
||||||
|
|
||||||
|
app.get(Endpoints.RESET_PASSWORD_REQUEST_GET, ResetPasswordRequestPost.default);
|
||||||
|
app.post(Endpoints.RESET_PASSWORD_FORM_POST, ResetPasswordFormPost.default);
|
||||||
|
|
||||||
|
app.get(Endpoints.VERIFY_GET, VerifyGet.default);
|
||||||
|
|
||||||
|
app.post(Endpoints.FIRST_FACTOR_POST, FirstFactorPost.default);
|
||||||
|
|
||||||
|
|
||||||
|
app.post(Endpoints.SECOND_FACTOR_TOTP_POST, TOTPSignGet.default);
|
||||||
|
|
||||||
|
|
||||||
|
app.get(Endpoints.SECOND_FACTOR_U2F_SIGN_REQUEST_GET, U2FSignRequestGet.default);
|
||||||
|
app.post(Endpoints.SECOND_FACTOR_U2F_SIGN_POST, U2FSignPost.default);
|
||||||
|
|
||||||
|
app.get(Endpoints.SECOND_FACTOR_U2F_REGISTER_REQUEST_GET, U2FRegisterRequestGet.default);
|
||||||
|
app.post(Endpoints.SECOND_FACTOR_U2F_REGISTER_POST, U2FRegisterPost.default);
|
||||||
|
|
||||||
|
app.get(Endpoints.ERROR_401_GET, Error401Get.default);
|
||||||
|
app.get(Endpoints.ERROR_403_GET, Error403Get.default);
|
||||||
|
app.get(Endpoints.ERROR_404_GET, Error404Get.default);
|
||||||
|
}
|
||||||
|
}
|
70
src/server/lib/Server.ts
Normal file
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
103
src/server/lib/ServerVariables.ts
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
|
||||||
|
import winston = require("winston");
|
||||||
|
import { LdapClient } from "./LdapClient";
|
||||||
|
import { TOTPValidator } from "./TOTPValidator";
|
||||||
|
import { TOTPGenerator } from "./TOTPGenerator";
|
||||||
|
import U2F = require("u2f");
|
||||||
|
import UserDataStore from "./UserDataStore";
|
||||||
|
import { INotifier } from "./notifiers/INotifier";
|
||||||
|
import { AuthenticationRegulator } from "./AuthenticationRegulator";
|
||||||
|
import Configuration = require("../../types/Configuration");
|
||||||
|
import { AccessController } from "./access_control/AccessController";
|
||||||
|
import { NotifierFactory } from "./notifiers/NotifierFactory";
|
||||||
|
|
||||||
|
import { GlobalDependencies } from "../../types/Dependencies";
|
||||||
|
|
||||||
|
import express = require("express");
|
||||||
|
|
||||||
|
export const VARIABLES_KEY = "authelia-variables";
|
||||||
|
|
||||||
|
export interface ServerVariables {
|
||||||
|
logger: typeof winston;
|
||||||
|
ldap: LdapClient;
|
||||||
|
totpValidator: TOTPValidator;
|
||||||
|
totpGenerator: TOTPGenerator;
|
||||||
|
u2f: typeof U2F;
|
||||||
|
userDataStore: UserDataStore;
|
||||||
|
notifier: INotifier;
|
||||||
|
regulator: AuthenticationRegulator;
|
||||||
|
config: Configuration.AppConfiguration;
|
||||||
|
accessController: AccessController;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function fill(app: express.Application, config: Configuration.AppConfiguration, deps: GlobalDependencies) {
|
||||||
|
const five_minutes = 5 * 60;
|
||||||
|
const datastore_options = {
|
||||||
|
directory: config.store_directory,
|
||||||
|
inMemory: config.store_in_memory
|
||||||
|
};
|
||||||
|
|
||||||
|
const userDataStore = new UserDataStore(datastore_options, deps.nedb);
|
||||||
|
const regulator = new AuthenticationRegulator(userDataStore, five_minutes);
|
||||||
|
const notifier = NotifierFactory.build(config.notifier, deps.nodemailer);
|
||||||
|
const ldap = new LdapClient(config.ldap, deps.ldapjs, deps.winston);
|
||||||
|
const accessController = new AccessController(config.access_control, deps.winston);
|
||||||
|
const totpValidator = new TOTPValidator(deps.speakeasy);
|
||||||
|
const totpGenerator = new TOTPGenerator(deps.speakeasy);
|
||||||
|
|
||||||
|
const variables: ServerVariables = {
|
||||||
|
accessController: accessController,
|
||||||
|
config: config,
|
||||||
|
ldap: ldap,
|
||||||
|
logger: deps.winston,
|
||||||
|
notifier: notifier,
|
||||||
|
regulator: regulator,
|
||||||
|
totpGenerator: totpGenerator,
|
||||||
|
totpValidator: totpValidator,
|
||||||
|
u2f: deps.u2f,
|
||||||
|
userDataStore: userDataStore
|
||||||
|
};
|
||||||
|
|
||||||
|
app.set(VARIABLES_KEY, variables);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLogger(app: express.Application): typeof winston {
|
||||||
|
return (app.get(VARIABLES_KEY) as ServerVariables).logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getUserDataStore(app: express.Application): UserDataStore {
|
||||||
|
return (app.get(VARIABLES_KEY) as ServerVariables).userDataStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getNotifier(app: express.Application): INotifier {
|
||||||
|
return (app.get(VARIABLES_KEY) as ServerVariables).notifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLdapClient(app: express.Application): LdapClient {
|
||||||
|
return (app.get(VARIABLES_KEY) as ServerVariables).ldap;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getConfiguration(app: express.Application): Configuration.AppConfiguration {
|
||||||
|
return (app.get(VARIABLES_KEY) as ServerVariables).config;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAuthenticationRegulator(app: express.Application): AuthenticationRegulator {
|
||||||
|
return (app.get(VARIABLES_KEY) as ServerVariables).regulator;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAccessController(app: express.Application): AccessController {
|
||||||
|
return (app.get(VARIABLES_KEY) as ServerVariables).accessController;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTOTPGenerator(app: express.Application): TOTPGenerator {
|
||||||
|
return (app.get(VARIABLES_KEY) as ServerVariables).totpGenerator;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTOTPValidator(app: express.Application): TOTPValidator {
|
||||||
|
return (app.get(VARIABLES_KEY) as ServerVariables).totpValidator;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getU2F(app: express.Application): typeof U2F {
|
||||||
|
return (app.get(VARIABLES_KEY) as ServerVariables).u2f;
|
||||||
|
}
|
|
@ -1,16 +1,16 @@
|
||||||
|
|
||||||
import * as speakeasy from "speakeasy";
|
import * as speakeasy from "speakeasy";
|
||||||
import { Speakeasy } from "../types/Dependencies";
|
import { Speakeasy } from "../../types/Dependencies";
|
||||||
import BluebirdPromise = require("bluebird");
|
import BluebirdPromise = require("bluebird");
|
||||||
|
|
||||||
export default class TOTPGenerator {
|
export class TOTPGenerator {
|
||||||
private speakeasy: Speakeasy;
|
private speakeasy: Speakeasy;
|
||||||
|
|
||||||
constructor(speakeasy: Speakeasy) {
|
constructor(speakeasy: Speakeasy) {
|
||||||
this.speakeasy = speakeasy;
|
this.speakeasy = speakeasy;
|
||||||
}
|
}
|
||||||
|
|
||||||
generate(options: speakeasy.GenerateOptions): speakeasy.Key {
|
generate(options?: speakeasy.GenerateOptions): speakeasy.Key {
|
||||||
return this.speakeasy.generateSecret(options);
|
return this.speakeasy.generateSecret(options);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,10 +1,10 @@
|
||||||
|
|
||||||
import { Speakeasy } from "../types/Dependencies";
|
import { Speakeasy } from "../../types/Dependencies";
|
||||||
import BluebirdPromise = require("bluebird");
|
import BluebirdPromise = require("bluebird");
|
||||||
|
|
||||||
const TOTP_ENCODING = "base32";
|
const TOTP_ENCODING = "base32";
|
||||||
|
|
||||||
export default class TOTPValidator {
|
export class TOTPValidator {
|
||||||
private speakeasy: Speakeasy;
|
private speakeasy: Speakeasy;
|
||||||
|
|
||||||
constructor(speakeasy: Speakeasy) {
|
constructor(speakeasy: Speakeasy) {
|