Added Theming functionality and theme folder
64
Gruntfile.js
|
@ -82,27 +82,51 @@ module.exports = function (grunt) {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
copy: {
|
copy: {
|
||||||
resources: {
|
main_resources: {
|
||||||
expand: true,
|
expand: true,
|
||||||
cwd: 'server/src/resources/',
|
cwd: 'themes/main/server/src/resources',
|
||||||
src: '**',
|
src: '**',
|
||||||
dest: `${buildDir}/server/src/resources/`
|
dest: `${buildDir}/server/src/resources/`
|
||||||
},
|
},
|
||||||
views: {
|
main_views: {
|
||||||
expand: true,
|
expand: true,
|
||||||
cwd: 'server/src/views/',
|
cwd: 'themes/main/server/src/views',
|
||||||
src: '**',
|
src: '**',
|
||||||
dest: `${buildDir}/server/src/views/`
|
dest: `${buildDir}/server/src/views/`
|
||||||
},
|
},
|
||||||
images: {
|
main_images: {
|
||||||
expand: true,
|
expand: true,
|
||||||
cwd: 'client/src/img',
|
cwd: 'themes/main/client/src/img',
|
||||||
src: '**',
|
src: '**',
|
||||||
dest: `${buildDir}/server/src/public_html/img/`
|
dest: `${buildDir}/server/src/public_html/img/`
|
||||||
},
|
},
|
||||||
thirdparties: {
|
main_thirdparties: {
|
||||||
expand: true,
|
expand: true,
|
||||||
cwd: 'client/src/thirdparties',
|
cwd: 'themes/main/client/src/thirdparties',
|
||||||
|
src: '**',
|
||||||
|
dest: `${buildDir}/server/src/public_html/js/`
|
||||||
|
},
|
||||||
|
matrix_resources: {
|
||||||
|
expand: true,
|
||||||
|
cwd: 'themes/matrix/server/src/resources',
|
||||||
|
src: '**',
|
||||||
|
dest: `${buildDir}/server/src/resources/`
|
||||||
|
},
|
||||||
|
matrix_views: {
|
||||||
|
expand: true,
|
||||||
|
cwd: 'themes/matrix/server/src/views',
|
||||||
|
src: '**',
|
||||||
|
dest: `${buildDir}/server/src/views/`
|
||||||
|
},
|
||||||
|
matrix_images: {
|
||||||
|
expand: true,
|
||||||
|
cwd: 'themes/matrix/client/src/img',
|
||||||
|
src: '**',
|
||||||
|
dest: `${buildDir}/server/src/public_html/img/`
|
||||||
|
},
|
||||||
|
matrix_thirdparties: {
|
||||||
|
expand: true,
|
||||||
|
cwd: 'themes/matrix/client/src/thirdparties',
|
||||||
src: '**',
|
src: '**',
|
||||||
dest: `${buildDir}/server/src/public_html/js/`
|
dest: `${buildDir}/server/src/public_html/js/`
|
||||||
},
|
},
|
||||||
|
@ -173,8 +197,14 @@ module.exports = function (grunt) {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
concat: {
|
concat: {
|
||||||
css: {
|
main_css: {
|
||||||
src: ['client/src/css/*.css'],
|
src: ['themes/main/client/src/css/*.css'],
|
||||||
|
dest: `${buildDir}/server/src/public_html/css/authelia.css`
|
||||||
|
},
|
||||||
|
},
|
||||||
|
concat: {
|
||||||
|
matrix_css: {
|
||||||
|
src: ['themes/matrix/client/src/css/*.css'],
|
||||||
dest: `${buildDir}/server/src/public_html/css/authelia.css`
|
dest: `${buildDir}/server/src/public_html/css/authelia.css`
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -187,6 +217,8 @@ module.exports = function (grunt) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
var target = grunt.option('target') || 'main';
|
||||||
|
|
||||||
grunt.loadNpmTasks('grunt-browserify');
|
grunt.loadNpmTasks('grunt-browserify');
|
||||||
grunt.loadNpmTasks('grunt-contrib-concat');
|
grunt.loadNpmTasks('grunt-contrib-concat');
|
||||||
grunt.loadNpmTasks('grunt-contrib-copy');
|
grunt.loadNpmTasks('grunt-contrib-copy');
|
||||||
|
@ -205,13 +237,17 @@ module.exports = function (grunt) {
|
||||||
grunt.registerTask('test-unit', ['test-server', 'test-client', 'test-shared']);
|
grunt.registerTask('test-unit', ['test-server', 'test-client', 'test-shared']);
|
||||||
grunt.registerTask('test-int', ['run:test-cucumber', 'run:test-minimal-config', 'run:test-complete-config', 'run:test-inactivity']);
|
grunt.registerTask('test-int', ['run:test-cucumber', 'run:test-minimal-config', 'run:test-complete-config', 'run:test-inactivity']);
|
||||||
|
|
||||||
grunt.registerTask('copy-resources', ['copy:resources', 'copy:views', 'copy:images', 'copy:thirdparties', 'concat:css']);
|
grunt.registerTask('copy-resources-main', ['copy:main_resources', 'copy:main_views', 'copy:main_images', 'copy:main_thirdparties', 'concat:main_css']);
|
||||||
|
|
||||||
grunt.registerTask('generate-config-schema', ['run:generate-config-schema', 'copy:schema']);
|
grunt.registerTask('generate-config-schema', ['run:generate-config-schema', 'copy:schema']);
|
||||||
|
|
||||||
grunt.registerTask('build-client', ['compile-client', 'browserify']);
|
grunt.registerTask('copy-resources-matrix', ['copy:matrix_resources', 'copy:matrix_views', 'copy:matrix_images', 'copy:matrix_thirdparties', 'concat:matrix_css']);
|
||||||
grunt.registerTask('build-server', ['compile-server', 'copy-resources', 'generate-config-schema']);
|
|
||||||
|
|
||||||
grunt.registerTask('build', ['build-client', 'build-server']);
|
grunt.registerTask('build-client', ['compile-client', 'browserify']);
|
||||||
|
grunt.registerTask('build-server-main', ['compile-server', 'copy-resources-main', 'generate-config-schema']);
|
||||||
|
grunt.registerTask('build-server-matrix', ['compile-server', 'copy-resources-matrix', 'generate-config-schema']);
|
||||||
|
|
||||||
|
grunt.registerTask('build', ['build-client', 'build-server-'+target]);
|
||||||
grunt.registerTask('build-dist', ['build', 'run:minify', 'cssmin', 'run:include-minified-script']);
|
grunt.registerTask('build-dist', ['build', 'run:minify', 'cssmin', 'run:include-minified-script']);
|
||||||
|
|
||||||
grunt.registerTask('schema', ['run:generate-config-schema'])
|
grunt.registerTask('schema', ['run:generate-config-schema'])
|
||||||
|
|
|
@ -2971,12 +2971,14 @@
|
||||||
"balanced-match": {
|
"balanced-match": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"brace-expansion": {
|
"brace-expansion": {
|
||||||
"version": "1.1.11",
|
"version": "1.1.11",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"balanced-match": "^1.0.0",
|
"balanced-match": "^1.0.0",
|
||||||
"concat-map": "0.0.1"
|
"concat-map": "0.0.1"
|
||||||
|
@ -2991,17 +2993,20 @@
|
||||||
"code-point-at": {
|
"code-point-at": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"concat-map": {
|
"concat-map": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"console-control-strings": {
|
"console-control-strings": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"core-util-is": {
|
"core-util-is": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
|
@ -3118,7 +3123,8 @@
|
||||||
"inherits": {
|
"inherits": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"ini": {
|
"ini": {
|
||||||
"version": "1.3.5",
|
"version": "1.3.5",
|
||||||
|
@ -3130,6 +3136,7 @@
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"number-is-nan": "^1.0.0"
|
"number-is-nan": "^1.0.0"
|
||||||
}
|
}
|
||||||
|
@ -3144,6 +3151,7 @@
|
||||||
"version": "3.0.4",
|
"version": "3.0.4",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"brace-expansion": "^1.1.7"
|
"brace-expansion": "^1.1.7"
|
||||||
}
|
}
|
||||||
|
@ -3151,12 +3159,14 @@
|
||||||
"minimist": {
|
"minimist": {
|
||||||
"version": "0.0.8",
|
"version": "0.0.8",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"minipass": {
|
"minipass": {
|
||||||
"version": "2.2.4",
|
"version": "2.2.4",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"safe-buffer": "^5.1.1",
|
"safe-buffer": "^5.1.1",
|
||||||
"yallist": "^3.0.0"
|
"yallist": "^3.0.0"
|
||||||
|
@ -3175,6 +3185,7 @@
|
||||||
"version": "0.5.1",
|
"version": "0.5.1",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"minimist": "0.0.8"
|
"minimist": "0.0.8"
|
||||||
}
|
}
|
||||||
|
@ -3255,7 +3266,8 @@
|
||||||
"number-is-nan": {
|
"number-is-nan": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"object-assign": {
|
"object-assign": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
|
@ -3267,6 +3279,7 @@
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"wrappy": "1"
|
"wrappy": "1"
|
||||||
}
|
}
|
||||||
|
@ -3388,6 +3401,7 @@
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"code-point-at": "^1.0.0",
|
"code-point-at": "^1.0.0",
|
||||||
"is-fullwidth-code-point": "^1.0.0",
|
"is-fullwidth-code-point": "^1.0.0",
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
[Dolphin]
|
||||||
|
Timestamp=2018,12,17,20,56,39
|
||||||
|
Version=3
|
||||||
|
ViewMode=1
|
|
@ -0,0 +1,67 @@
|
||||||
|
body {
|
||||||
|
background-image: url("/img/background.svg");
|
||||||
|
}
|
||||||
|
.authelia-brand {
|
||||||
|
font-weight: bold;
|
||||||
|
font-style: italic;
|
||||||
|
color: #648caf
|
||||||
|
}
|
||||||
|
.poweredby-block {
|
||||||
|
margin: 0px 30px;
|
||||||
|
margin-top: 10px;
|
||||||
|
padding-top: 15px;
|
||||||
|
border-top: 1px solid rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
.poweredby {
|
||||||
|
font-size: 0.7em;
|
||||||
|
color: #6b6b6b;
|
||||||
|
}
|
||||||
|
/* notifications */
|
||||||
|
.notification {
|
||||||
|
padding: 10px;
|
||||||
|
margin: 15px 0px;
|
||||||
|
border-radius: 6px;
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
.notification img {
|
||||||
|
width: 24px;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
.notification i,
|
||||||
|
.notification span {
|
||||||
|
display:table-cell;
|
||||||
|
vertical-align:middle;
|
||||||
|
}
|
||||||
|
.info {
|
||||||
|
border: 1px solid #9cb1ff;
|
||||||
|
background-color: rgb(192, 220, 255);
|
||||||
|
}
|
||||||
|
.success {
|
||||||
|
border: 1px solid #65ec7c;
|
||||||
|
background-color: rgb(163, 255, 157);
|
||||||
|
}
|
||||||
|
.error {
|
||||||
|
border: 1px solid #ffa3a3;
|
||||||
|
background-color: rgb(255, 175, 175);
|
||||||
|
}
|
||||||
|
.warning {
|
||||||
|
border: 1px solid #ffd743;
|
||||||
|
background-color: rgb(255, 230, 143);
|
||||||
|
}
|
||||||
|
.bottom-right-links {
|
||||||
|
text-align: right;
|
||||||
|
margin-top: 10px;
|
||||||
|
font-size: 0.8em;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
background-color: #778dab;
|
||||||
|
color: white;
|
||||||
|
margin: 0px;
|
||||||
|
}
|
||||||
|
.body {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
font-size: 25px;
|
||||||
|
}
|
|
@ -0,0 +1,132 @@
|
||||||
|
.form-signin
|
||||||
|
{
|
||||||
|
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-bottom: 20px;
|
||||||
|
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
|
||||||
|
{
|
||||||
|
margin-bottom: 15px;
|
||||||
|
margin-top: 15px;
|
||||||
|
font-weight: 800;
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.account-wall h3
|
||||||
|
{
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.account-wall p
|
||||||
|
{
|
||||||
|
text-align: center;
|
||||||
|
margin: 10px;
|
||||||
|
}
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.u2f-token {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.u2f-token img {
|
||||||
|
width: 70px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keep-me-logged-in {
|
||||||
|
margin-top: 10px;
|
||||||
|
font-size: 0.8em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keep-me-logged-in input[type=checkbox] {
|
||||||
|
transform: scale(0.8);
|
||||||
|
margin: 0;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keep-me-logged-in label {
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keep-me-logged-in input,
|
||||||
|
.keep-me-logged-in label {
|
||||||
|
display: inline-block;
|
||||||
|
margin-bottom: 0; /* I added this after I posted my reply */
|
||||||
|
vertical-align: middle; /* Fixes any weird issues in Firefox and IE */
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
|
||||||
|
.error-401 .header-img {
|
||||||
|
border-radius: 0%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-403 .header-img {
|
||||||
|
border-radius: 0%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-404 .header-img {
|
||||||
|
border-radius: 0%;
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
|
||||||
|
.password-reset-form .header-img {
|
||||||
|
border-radius: 0%;
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
|
||||||
|
.password-reset-request .header-img {
|
||||||
|
border-radius: 0%;
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
.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: 10px auto;
|
||||||
|
}
|
||||||
|
.totp-register .need-google-authenticator {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
.totp-register .store-badges {
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
.totp-register .store-badge {
|
||||||
|
width: 110px;
|
||||||
|
height: 30px;
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
|
||||||
|
.u2f-register img {
|
||||||
|
display: block;
|
||||||
|
margin: 20px auto;
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="56" height="100">
|
||||||
|
<rect width="56" height="100" fill="#FFFFFF"></rect>
|
||||||
|
<path d="M28 66L0 50L0 16L28 0L56 16L56 50L28 66L28 100" fill="none" stroke="#FCFCFC" stroke-width="2"></path>
|
||||||
|
<path d="M28 0L28 34L0 50L0 84L28 100L56 84L56 50L28 34" fill="none" stroke="#FBFBFB" stroke-width="2"></path>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 347 B |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 3.5 KiB |
|
@ -0,0 +1,4 @@
|
||||||
|
[Dolphin]
|
||||||
|
Timestamp=2018,12,17,20,57,31
|
||||||
|
Version=3
|
||||||
|
ViewMode=1
|
After Width: | Height: | Size: 863 B |
After Width: | Height: | Size: 732 B |
After Width: | Height: | Size: 931 B |
After Width: | Height: | Size: 580 B |
After Width: | Height: | Size: 3.2 KiB |
After Width: | Height: | Size: 2.1 KiB |
After Width: | Height: | Size: 6.6 KiB |
|
@ -0,0 +1,4 @@
|
||||||
|
[Dolphin]
|
||||||
|
Timestamp=2018,12,17,20,57,26
|
||||||
|
Version=3
|
||||||
|
ViewMode=1
|
|
@ -0,0 +1,129 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg version="1.1" id="US_UK_Download_on_the" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
x="0px" y="0px" width="135px" height="40px" viewBox="0 0 135 40" enable-background="new 0 0 135 40" xml:space="preserve">
|
||||||
|
<g>
|
||||||
|
<path fill="#A6A6A6" d="M130.197,40H4.729C2.122,40,0,37.872,0,35.267V4.726C0,2.12,2.122,0,4.729,0h125.468
|
||||||
|
C132.803,0,135,2.12,135,4.726v30.541C135,37.872,132.803,40,130.197,40L130.197,40z"/>
|
||||||
|
<path d="M134.032,35.268c0,2.116-1.714,3.83-3.834,3.83H4.729c-2.119,0-3.839-1.714-3.839-3.83V4.725
|
||||||
|
c0-2.115,1.72-3.835,3.839-3.835h125.468c2.121,0,3.834,1.72,3.834,3.835L134.032,35.268L134.032,35.268z"/>
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<path fill="#FFFFFF" d="M30.128,19.784c-0.029-3.223,2.639-4.791,2.761-4.864c-1.511-2.203-3.853-2.504-4.676-2.528
|
||||||
|
c-1.967-0.207-3.875,1.177-4.877,1.177c-1.022,0-2.565-1.157-4.228-1.123c-2.14,0.033-4.142,1.272-5.24,3.196
|
||||||
|
c-2.266,3.923-0.576,9.688,1.595,12.859c1.086,1.553,2.355,3.287,4.016,3.226c1.625-0.067,2.232-1.036,4.193-1.036
|
||||||
|
c1.943,0,2.513,1.036,4.207,0.997c1.744-0.028,2.842-1.56,3.89-3.127c1.255-1.78,1.759-3.533,1.779-3.623
|
||||||
|
C33.507,24.924,30.161,23.647,30.128,19.784z"/>
|
||||||
|
<path fill="#FFFFFF" d="M26.928,10.306c0.874-1.093,1.472-2.58,1.306-4.089c-1.265,0.056-2.847,0.875-3.758,1.944
|
||||||
|
c-0.806,0.942-1.526,2.486-1.34,3.938C24.557,12.205,26.016,11.382,26.928,10.306z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<path fill="#FFFFFF" d="M53.645,31.504h-2.271l-1.244-3.909h-4.324l-1.185,3.909h-2.211l4.284-13.308h2.646L53.645,31.504z
|
||||||
|
M49.755,25.955L48.63,22.48c-0.119-0.355-0.342-1.191-0.671-2.507h-0.04c-0.131,0.566-0.342,1.402-0.632,2.507l-1.105,3.475
|
||||||
|
H49.755z"/>
|
||||||
|
<path fill="#FFFFFF" d="M64.662,26.588c0,1.632-0.441,2.922-1.323,3.869c-0.79,0.843-1.771,1.264-2.942,1.264
|
||||||
|
c-1.264,0-2.172-0.454-2.725-1.362h-0.04v5.055h-2.132V25.067c0-1.026-0.027-2.079-0.079-3.159h1.875l0.119,1.521h0.04
|
||||||
|
c0.711-1.146,1.79-1.718,3.238-1.718c1.132,0,2.077,0.447,2.833,1.342C64.284,23.949,64.662,25.127,64.662,26.588z M62.49,26.666
|
||||||
|
c0-0.934-0.21-1.704-0.632-2.31c-0.461-0.632-1.08-0.948-1.856-0.948c-0.526,0-1.004,0.176-1.431,0.523
|
||||||
|
c-0.428,0.35-0.708,0.807-0.839,1.373c-0.066,0.264-0.099,0.48-0.099,0.65v1.6c0,0.698,0.214,1.287,0.642,1.768
|
||||||
|
s0.984,0.721,1.668,0.721c0.803,0,1.428-0.31,1.875-0.928C62.266,28.496,62.49,27.68,62.49,26.666z"/>
|
||||||
|
<path fill="#FFFFFF" d="M75.699,26.588c0,1.632-0.441,2.922-1.324,3.869c-0.789,0.843-1.77,1.264-2.941,1.264
|
||||||
|
c-1.264,0-2.172-0.454-2.724-1.362H68.67v5.055h-2.132V25.067c0-1.026-0.027-2.079-0.079-3.159h1.875l0.119,1.521h0.04
|
||||||
|
c0.71-1.146,1.789-1.718,3.238-1.718c1.131,0,2.076,0.447,2.834,1.342C75.32,23.949,75.699,25.127,75.699,26.588z M73.527,26.666
|
||||||
|
c0-0.934-0.211-1.704-0.633-2.31c-0.461-0.632-1.078-0.948-1.855-0.948c-0.527,0-1.004,0.176-1.432,0.523
|
||||||
|
c-0.428,0.35-0.707,0.807-0.838,1.373c-0.065,0.264-0.099,0.48-0.099,0.65v1.6c0,0.698,0.214,1.287,0.64,1.768
|
||||||
|
c0.428,0.48,0.984,0.721,1.67,0.721c0.803,0,1.428-0.31,1.875-0.928C73.303,28.496,73.527,27.68,73.527,26.666z"/>
|
||||||
|
<path fill="#FFFFFF" d="M88.039,27.772c0,1.132-0.393,2.053-1.182,2.764c-0.867,0.777-2.074,1.165-3.625,1.165
|
||||||
|
c-1.432,0-2.58-0.276-3.449-0.829l0.494-1.777c0.936,0.566,1.963,0.85,3.082,0.85c0.803,0,1.428-0.182,1.877-0.544
|
||||||
|
c0.447-0.362,0.67-0.848,0.67-1.454c0-0.54-0.184-0.995-0.553-1.364c-0.367-0.369-0.98-0.712-1.836-1.029
|
||||||
|
c-2.33-0.869-3.494-2.142-3.494-3.816c0-1.094,0.408-1.991,1.225-2.689c0.814-0.699,1.9-1.048,3.258-1.048
|
||||||
|
c1.211,0,2.217,0.211,3.02,0.632l-0.533,1.738c-0.75-0.408-1.598-0.612-2.547-0.612c-0.75,0-1.336,0.185-1.756,0.553
|
||||||
|
c-0.355,0.329-0.533,0.73-0.533,1.205c0,0.526,0.203,0.961,0.611,1.303c0.355,0.316,1,0.658,1.936,1.027
|
||||||
|
c1.145,0.461,1.986,1,2.527,1.618C87.77,26.081,88.039,26.852,88.039,27.772z"/>
|
||||||
|
<path fill="#FFFFFF" d="M95.088,23.508h-2.35v4.659c0,1.185,0.414,1.777,1.244,1.777c0.381,0,0.697-0.033,0.947-0.099l0.059,1.619
|
||||||
|
c-0.42,0.157-0.973,0.236-1.658,0.236c-0.842,0-1.5-0.257-1.975-0.77c-0.473-0.514-0.711-1.376-0.711-2.587v-4.837h-1.4v-1.6h1.4
|
||||||
|
v-1.757l2.094-0.632v2.389h2.35V23.508z"/>
|
||||||
|
<path fill="#FFFFFF" d="M105.691,26.627c0,1.475-0.422,2.686-1.264,3.633c-0.883,0.975-2.055,1.461-3.516,1.461
|
||||||
|
c-1.408,0-2.529-0.467-3.365-1.401s-1.254-2.113-1.254-3.534c0-1.487,0.43-2.705,1.293-3.652c0.861-0.948,2.023-1.422,3.484-1.422
|
||||||
|
c1.408,0,2.541,0.467,3.396,1.402C105.283,24.021,105.691,25.192,105.691,26.627z M103.479,26.696
|
||||||
|
c0-0.885-0.189-1.644-0.572-2.277c-0.447-0.766-1.086-1.148-1.914-1.148c-0.857,0-1.508,0.383-1.955,1.148
|
||||||
|
c-0.383,0.634-0.572,1.405-0.572,2.317c0,0.885,0.189,1.644,0.572,2.276c0.461,0.766,1.105,1.148,1.936,1.148
|
||||||
|
c0.814,0,1.453-0.39,1.914-1.168C103.281,28.347,103.479,27.58,103.479,26.696z"/>
|
||||||
|
<path fill="#FFFFFF" d="M112.621,23.783c-0.211-0.039-0.436-0.059-0.672-0.059c-0.75,0-1.33,0.283-1.738,0.85
|
||||||
|
c-0.355,0.5-0.533,1.132-0.533,1.895v5.035h-2.131l0.02-6.574c0-1.106-0.027-2.113-0.08-3.021h1.857l0.078,1.836h0.059
|
||||||
|
c0.225-0.631,0.58-1.139,1.066-1.52c0.475-0.343,0.988-0.514,1.541-0.514c0.197,0,0.375,0.014,0.533,0.039V23.783z"/>
|
||||||
|
<path fill="#FFFFFF" d="M122.156,26.252c0,0.382-0.025,0.704-0.078,0.967h-6.396c0.025,0.948,0.334,1.673,0.928,2.173
|
||||||
|
c0.539,0.447,1.236,0.671,2.092,0.671c0.947,0,1.811-0.151,2.588-0.454l0.334,1.48c-0.908,0.396-1.98,0.593-3.217,0.593
|
||||||
|
c-1.488,0-2.656-0.438-3.506-1.313c-0.848-0.875-1.273-2.05-1.273-3.524c0-1.447,0.395-2.652,1.186-3.613
|
||||||
|
c0.828-1.026,1.947-1.539,3.355-1.539c1.383,0,2.43,0.513,3.141,1.539C121.873,24.047,122.156,25.055,122.156,26.252z
|
||||||
|
M120.123,25.699c0.014-0.632-0.125-1.178-0.414-1.639c-0.369-0.593-0.936-0.889-1.699-0.889c-0.697,0-1.264,0.289-1.697,0.869
|
||||||
|
c-0.355,0.461-0.566,1.014-0.631,1.658H120.123z"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<path fill="#FFFFFF" d="M49.05,10.009c0,1.177-0.353,2.063-1.058,2.658c-0.653,0.549-1.581,0.824-2.783,0.824
|
||||||
|
c-0.596,0-1.106-0.026-1.533-0.078V6.982c0.557-0.09,1.157-0.136,1.805-0.136c1.145,0,2.008,0.249,2.59,0.747
|
||||||
|
C48.723,8.156,49.05,8.961,49.05,10.009z M47.945,10.038c0-0.763-0.202-1.348-0.606-1.756c-0.404-0.407-0.994-0.611-1.771-0.611
|
||||||
|
c-0.33,0-0.611,0.022-0.844,0.068v4.889c0.129,0.02,0.365,0.029,0.708,0.029c0.802,0,1.421-0.223,1.857-0.669
|
||||||
|
S47.945,10.892,47.945,10.038z"/>
|
||||||
|
<path fill="#FFFFFF" d="M54.909,11.037c0,0.725-0.207,1.319-0.621,1.785c-0.434,0.479-1.009,0.718-1.727,0.718
|
||||||
|
c-0.692,0-1.243-0.229-1.654-0.689c-0.41-0.459-0.615-1.038-0.615-1.736c0-0.73,0.211-1.329,0.635-1.794s0.994-0.698,1.712-0.698
|
||||||
|
c0.692,0,1.248,0.229,1.669,0.688C54.708,9.757,54.909,10.333,54.909,11.037z M53.822,11.071c0-0.435-0.094-0.808-0.281-1.119
|
||||||
|
c-0.22-0.376-0.533-0.564-0.94-0.564c-0.421,0-0.741,0.188-0.961,0.564c-0.188,0.311-0.281,0.69-0.281,1.138
|
||||||
|
c0,0.435,0.094,0.808,0.281,1.119c0.227,0.376,0.543,0.564,0.951,0.564c0.4,0,0.714-0.191,0.94-0.574
|
||||||
|
C53.725,11.882,53.822,11.506,53.822,11.071z"/>
|
||||||
|
<path fill="#FFFFFF" d="M62.765,8.719l-1.475,4.714h-0.96l-0.611-2.047c-0.155-0.511-0.281-1.019-0.379-1.523h-0.019
|
||||||
|
c-0.091,0.518-0.217,1.025-0.379,1.523l-0.649,2.047h-0.971l-1.387-4.714h1.077l0.533,2.241c0.129,0.53,0.235,1.035,0.32,1.513
|
||||||
|
h0.019c0.078-0.394,0.207-0.896,0.389-1.503l0.669-2.25h0.854l0.641,2.202c0.155,0.537,0.281,1.054,0.378,1.552h0.029
|
||||||
|
c0.071-0.485,0.178-1.002,0.32-1.552l0.572-2.202H62.765z"/>
|
||||||
|
<path fill="#FFFFFF" d="M68.198,13.433H67.15v-2.7c0-0.832-0.316-1.248-0.95-1.248c-0.311,0-0.562,0.114-0.757,0.343
|
||||||
|
c-0.193,0.229-0.291,0.499-0.291,0.808v2.796h-1.048v-3.366c0-0.414-0.013-0.863-0.038-1.349h0.921l0.049,0.737h0.029
|
||||||
|
c0.122-0.229,0.304-0.418,0.543-0.569c0.284-0.176,0.602-0.265,0.95-0.265c0.44,0,0.806,0.142,1.097,0.427
|
||||||
|
c0.362,0.349,0.543,0.87,0.543,1.562V13.433z"/>
|
||||||
|
<path fill="#FFFFFF" d="M71.088,13.433h-1.047V6.556h1.047V13.433z"/>
|
||||||
|
<path fill="#FFFFFF" d="M77.258,11.037c0,0.725-0.207,1.319-0.621,1.785c-0.434,0.479-1.01,0.718-1.727,0.718
|
||||||
|
c-0.693,0-1.244-0.229-1.654-0.689c-0.41-0.459-0.615-1.038-0.615-1.736c0-0.73,0.211-1.329,0.635-1.794s0.994-0.698,1.711-0.698
|
||||||
|
c0.693,0,1.248,0.229,1.67,0.688C77.057,9.757,77.258,10.333,77.258,11.037z M76.17,11.071c0-0.435-0.094-0.808-0.281-1.119
|
||||||
|
c-0.219-0.376-0.533-0.564-0.939-0.564c-0.422,0-0.742,0.188-0.961,0.564c-0.188,0.311-0.281,0.69-0.281,1.138
|
||||||
|
c0,0.435,0.094,0.808,0.281,1.119c0.227,0.376,0.543,0.564,0.951,0.564c0.4,0,0.713-0.191,0.939-0.574
|
||||||
|
C76.074,11.882,76.17,11.506,76.17,11.071z"/>
|
||||||
|
<path fill="#FFFFFF" d="M82.33,13.433h-0.941l-0.078-0.543h-0.029c-0.322,0.433-0.781,0.65-1.377,0.65
|
||||||
|
c-0.445,0-0.805-0.143-1.076-0.427c-0.246-0.258-0.369-0.579-0.369-0.96c0-0.576,0.24-1.015,0.723-1.319
|
||||||
|
c0.482-0.304,1.16-0.453,2.033-0.446V10.3c0-0.621-0.326-0.931-0.979-0.931c-0.465,0-0.875,0.117-1.229,0.349l-0.213-0.688
|
||||||
|
c0.438-0.271,0.979-0.407,1.617-0.407c1.232,0,1.85,0.65,1.85,1.95v1.736C82.262,12.78,82.285,13.155,82.33,13.433z
|
||||||
|
M81.242,11.813v-0.727c-1.156-0.02-1.734,0.297-1.734,0.95c0,0.246,0.066,0.43,0.201,0.553c0.135,0.123,0.307,0.184,0.512,0.184
|
||||||
|
c0.23,0,0.445-0.073,0.641-0.218c0.197-0.146,0.318-0.331,0.363-0.558C81.236,11.946,81.242,11.884,81.242,11.813z"/>
|
||||||
|
<path fill="#FFFFFF" d="M88.285,13.433h-0.93l-0.049-0.757h-0.029c-0.297,0.576-0.803,0.864-1.514,0.864
|
||||||
|
c-0.568,0-1.041-0.223-1.416-0.669s-0.562-1.025-0.562-1.736c0-0.763,0.203-1.381,0.611-1.853c0.395-0.44,0.879-0.66,1.455-0.66
|
||||||
|
c0.633,0,1.076,0.213,1.328,0.64h0.02V6.556h1.049v5.607C88.248,12.622,88.26,13.045,88.285,13.433z M87.199,11.445v-0.786
|
||||||
|
c0-0.136-0.01-0.246-0.029-0.33c-0.059-0.252-0.186-0.464-0.379-0.635c-0.195-0.171-0.43-0.257-0.701-0.257
|
||||||
|
c-0.391,0-0.697,0.155-0.922,0.466c-0.223,0.311-0.336,0.708-0.336,1.193c0,0.466,0.107,0.844,0.322,1.135
|
||||||
|
c0.227,0.31,0.533,0.465,0.916,0.465c0.344,0,0.619-0.129,0.828-0.388C87.1,12.069,87.199,11.781,87.199,11.445z"/>
|
||||||
|
<path fill="#FFFFFF" d="M97.248,11.037c0,0.725-0.207,1.319-0.621,1.785c-0.434,0.479-1.008,0.718-1.727,0.718
|
||||||
|
c-0.691,0-1.242-0.229-1.654-0.689c-0.41-0.459-0.615-1.038-0.615-1.736c0-0.73,0.211-1.329,0.635-1.794s0.994-0.698,1.713-0.698
|
||||||
|
c0.691,0,1.248,0.229,1.668,0.688C97.047,9.757,97.248,10.333,97.248,11.037z M96.162,11.071c0-0.435-0.094-0.808-0.281-1.119
|
||||||
|
c-0.221-0.376-0.533-0.564-0.941-0.564c-0.42,0-0.74,0.188-0.961,0.564c-0.188,0.311-0.281,0.69-0.281,1.138
|
||||||
|
c0,0.435,0.094,0.808,0.281,1.119c0.227,0.376,0.543,0.564,0.951,0.564c0.4,0,0.715-0.191,0.941-0.574
|
||||||
|
C96.064,11.882,96.162,11.506,96.162,11.071z"/>
|
||||||
|
<path fill="#FFFFFF" d="M102.883,13.433h-1.047v-2.7c0-0.832-0.316-1.248-0.951-1.248c-0.311,0-0.562,0.114-0.756,0.343
|
||||||
|
s-0.291,0.499-0.291,0.808v2.796h-1.049v-3.366c0-0.414-0.012-0.863-0.037-1.349h0.92l0.049,0.737h0.029
|
||||||
|
c0.123-0.229,0.305-0.418,0.543-0.569c0.285-0.176,0.602-0.265,0.951-0.265c0.439,0,0.805,0.142,1.096,0.427
|
||||||
|
c0.363,0.349,0.543,0.87,0.543,1.562V13.433z"/>
|
||||||
|
<path fill="#FFFFFF" d="M109.936,9.504h-1.154v2.29c0,0.582,0.205,0.873,0.611,0.873c0.188,0,0.344-0.016,0.467-0.049
|
||||||
|
l0.027,0.795c-0.207,0.078-0.479,0.117-0.814,0.117c-0.414,0-0.736-0.126-0.969-0.378c-0.234-0.252-0.35-0.676-0.35-1.271V9.504
|
||||||
|
h-0.689V8.719h0.689V7.855l1.027-0.31v1.173h1.154V9.504z"/>
|
||||||
|
<path fill="#FFFFFF" d="M115.484,13.433h-1.049v-2.68c0-0.845-0.316-1.268-0.949-1.268c-0.486,0-0.818,0.245-1,0.735
|
||||||
|
c-0.031,0.103-0.049,0.229-0.049,0.377v2.835h-1.047V6.556h1.047v2.841h0.02c0.33-0.517,0.803-0.775,1.416-0.775
|
||||||
|
c0.434,0,0.793,0.142,1.078,0.427c0.355,0.355,0.533,0.883,0.533,1.581V13.433z"/>
|
||||||
|
<path fill="#FFFFFF" d="M121.207,10.853c0,0.188-0.014,0.346-0.039,0.475h-3.143c0.014,0.466,0.164,0.821,0.455,1.067
|
||||||
|
c0.266,0.22,0.609,0.33,1.029,0.33c0.465,0,0.889-0.074,1.271-0.223l0.164,0.728c-0.447,0.194-0.973,0.291-1.582,0.291
|
||||||
|
c-0.73,0-1.305-0.215-1.721-0.645c-0.418-0.43-0.625-1.007-0.625-1.731c0-0.711,0.193-1.303,0.582-1.775
|
||||||
|
c0.406-0.504,0.955-0.756,1.648-0.756c0.678,0,1.193,0.252,1.541,0.756C121.068,9.77,121.207,10.265,121.207,10.853z
|
||||||
|
M120.207,10.582c0.008-0.311-0.061-0.579-0.203-0.805c-0.182-0.291-0.459-0.437-0.834-0.437c-0.342,0-0.621,0.142-0.834,0.427
|
||||||
|
c-0.174,0.227-0.277,0.498-0.311,0.815H120.207z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 12 KiB |
|
@ -0,0 +1,429 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
id="svg2"
|
||||||
|
version="1.1"
|
||||||
|
inkscape:version="0.91 r13725"
|
||||||
|
xml:space="preserve"
|
||||||
|
width="135.71649"
|
||||||
|
height="40.018951"
|
||||||
|
viewBox="0 0 135.71649 40.018951"
|
||||||
|
sodipodi:docname="google-play-badge.svg"><metadata
|
||||||
|
id="metadata8"><rdf:RDF><cc:Work
|
||||||
|
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
|
||||||
|
id="defs6"><linearGradient
|
||||||
|
x1="31.7997"
|
||||||
|
y1="183.2903"
|
||||||
|
x2="15.0173"
|
||||||
|
y2="166.5079"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
gradientTransform="matrix(0.8,0,0,-0.8,0,161.6)"
|
||||||
|
spreadMethod="pad"
|
||||||
|
id="linearGradient50"><stop
|
||||||
|
style="stop-opacity:1;stop-color:#00a0ff"
|
||||||
|
offset="0"
|
||||||
|
id="stop52" /><stop
|
||||||
|
style="stop-opacity:1;stop-color:#00a1ff"
|
||||||
|
offset="0.0066"
|
||||||
|
id="stop54" /><stop
|
||||||
|
style="stop-opacity:1;stop-color:#00beff"
|
||||||
|
offset="0.2601"
|
||||||
|
id="stop56" /><stop
|
||||||
|
style="stop-opacity:1;stop-color:#00d2ff"
|
||||||
|
offset="0.5122"
|
||||||
|
id="stop58" /><stop
|
||||||
|
style="stop-opacity:1;stop-color:#00dfff"
|
||||||
|
offset="0.7604"
|
||||||
|
id="stop60" /><stop
|
||||||
|
style="stop-opacity:1;stop-color:#00e3ff"
|
||||||
|
offset="1"
|
||||||
|
id="stop62" /></linearGradient><linearGradient
|
||||||
|
x1="43.8344"
|
||||||
|
y1="171.9986"
|
||||||
|
x2="19.637501"
|
||||||
|
y2="171.9986"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
gradientTransform="matrix(0.8,0,0,-0.8,0,161.6)"
|
||||||
|
spreadMethod="pad"
|
||||||
|
id="linearGradient68"><stop
|
||||||
|
style="stop-opacity:1;stop-color:#ffe000"
|
||||||
|
offset="0"
|
||||||
|
id="stop70" /><stop
|
||||||
|
style="stop-opacity:1;stop-color:#ffbd00"
|
||||||
|
offset="0.4087"
|
||||||
|
id="stop72" /><stop
|
||||||
|
style="stop-opacity:1;stop-color:#ffa500"
|
||||||
|
offset="0.7754"
|
||||||
|
id="stop74" /><stop
|
||||||
|
style="stop-opacity:1;stop-color:#ff9c00"
|
||||||
|
offset="1"
|
||||||
|
id="stop76" /></linearGradient><linearGradient
|
||||||
|
x1="34.827"
|
||||||
|
y1="169.7039"
|
||||||
|
x2="12.0687"
|
||||||
|
y2="146.9456"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
gradientTransform="matrix(0.8,0,0,-0.8,0,161.6)"
|
||||||
|
spreadMethod="pad"
|
||||||
|
id="linearGradient82"><stop
|
||||||
|
style="stop-opacity:1;stop-color:#ff3a44"
|
||||||
|
offset="0"
|
||||||
|
id="stop84" /><stop
|
||||||
|
style="stop-opacity:1;stop-color:#c31162"
|
||||||
|
offset="1"
|
||||||
|
id="stop86" /></linearGradient><linearGradient
|
||||||
|
x1="17.2973"
|
||||||
|
y1="191.82381"
|
||||||
|
x2="27.4599"
|
||||||
|
y2="181.6613"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
gradientTransform="matrix(0.8,0,0,-0.8,0,161.6)"
|
||||||
|
spreadMethod="pad"
|
||||||
|
id="linearGradient92"><stop
|
||||||
|
style="stop-opacity:1;stop-color:#32a071"
|
||||||
|
offset="0"
|
||||||
|
id="stop94" /><stop
|
||||||
|
style="stop-opacity:1;stop-color:#2da771"
|
||||||
|
offset="0.0685"
|
||||||
|
id="stop96" /><stop
|
||||||
|
style="stop-opacity:1;stop-color:#15cf74"
|
||||||
|
offset="0.4762"
|
||||||
|
id="stop98" /><stop
|
||||||
|
style="stop-opacity:1;stop-color:#06e775"
|
||||||
|
offset="0.8009"
|
||||||
|
id="stop100" /><stop
|
||||||
|
style="stop-opacity:1;stop-color:#00f076"
|
||||||
|
offset="1"
|
||||||
|
id="stop102" /></linearGradient><clipPath
|
||||||
|
clipPathUnits="userSpaceOnUse"
|
||||||
|
id="clipPath110"><path
|
||||||
|
d="M 0,0 124,0 124,48 0,48 0,0 Z"
|
||||||
|
id="path112"
|
||||||
|
inkscape:connector-curvature="0" /></clipPath><mask
|
||||||
|
maskUnits="userSpaceOnUse"
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
width="1"
|
||||||
|
height="1"
|
||||||
|
id="mask114"><g
|
||||||
|
id="g116"><g
|
||||||
|
clip-path="url(#clipPath110)"
|
||||||
|
id="g118"><path
|
||||||
|
d="M 0,0 124,0 124,48 0,48 0,0 Z"
|
||||||
|
style="fill:#000000;fill-opacity:0.2;fill-rule:nonzero;stroke:none"
|
||||||
|
id="path120"
|
||||||
|
inkscape:connector-curvature="0" /></g></g></mask><clipPath
|
||||||
|
clipPathUnits="userSpaceOnUse"
|
||||||
|
id="clipPath126"><path
|
||||||
|
d="M 0,0 124,0 124,48 0,48 0,0 Z"
|
||||||
|
id="path128"
|
||||||
|
inkscape:connector-curvature="0" /></clipPath><clipPath
|
||||||
|
clipPathUnits="userSpaceOnUse"
|
||||||
|
id="clipPath130"><path
|
||||||
|
d="M 0,0 124,0 124,48 0,48 0,0 Z"
|
||||||
|
id="path132"
|
||||||
|
inkscape:connector-curvature="0" /></clipPath><pattern
|
||||||
|
patternTransform="matrix(1,0,0,-1,0,48)"
|
||||||
|
patternUnits="userSpaceOnUse"
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
width="124"
|
||||||
|
height="48"
|
||||||
|
id="pattern134"><g
|
||||||
|
id="g136" /><g
|
||||||
|
id="g138"><g
|
||||||
|
clip-path="url(#clipPath130)"
|
||||||
|
id="g140"><g
|
||||||
|
id="g142"><path
|
||||||
|
d="M 29.625,20.695 18.012,14.098 C 17.363,13.727 16.781,13.754 16.406,14.09 l -0.058,-0.063 0.058,-0.058 c 0.375,-0.336 0.957,-0.36 1.606,0.011 l 11.687,6.641 -0.074,0.074 z"
|
||||||
|
style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||||
|
id="path144" /></g></g></g></pattern><clipPath
|
||||||
|
clipPathUnits="userSpaceOnUse"
|
||||||
|
id="clipPath158"><path
|
||||||
|
d="M 0,0 124,0 124,48 0,48 0,0 Z"
|
||||||
|
id="path160"
|
||||||
|
inkscape:connector-curvature="0" /></clipPath><mask
|
||||||
|
maskUnits="userSpaceOnUse"
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
width="1"
|
||||||
|
height="1"
|
||||||
|
id="mask162"><g
|
||||||
|
id="g164"><g
|
||||||
|
clip-path="url(#clipPath158)"
|
||||||
|
id="g166"><path
|
||||||
|
d="M 0,0 124,0 124,48 0,48 0,0 Z"
|
||||||
|
style="fill:#000000;fill-opacity:0.12000002;fill-rule:nonzero;stroke:none"
|
||||||
|
id="path168"
|
||||||
|
inkscape:connector-curvature="0" /></g></g></mask><clipPath
|
||||||
|
clipPathUnits="userSpaceOnUse"
|
||||||
|
id="clipPath174"><path
|
||||||
|
d="M 0,0 124,0 124,48 0,48 0,0 Z"
|
||||||
|
id="path176"
|
||||||
|
inkscape:connector-curvature="0" /></clipPath><clipPath
|
||||||
|
clipPathUnits="userSpaceOnUse"
|
||||||
|
id="clipPath178"><path
|
||||||
|
d="M 0,0 124,0 124,48 0,48 0,0 Z"
|
||||||
|
id="path180"
|
||||||
|
inkscape:connector-curvature="0" /></clipPath><pattern
|
||||||
|
patternTransform="matrix(1,0,0,-1,0,48)"
|
||||||
|
patternUnits="userSpaceOnUse"
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
width="124"
|
||||||
|
height="48"
|
||||||
|
id="pattern182"><g
|
||||||
|
id="g184" /><g
|
||||||
|
id="g186"><g
|
||||||
|
clip-path="url(#clipPath178)"
|
||||||
|
id="g188"><g
|
||||||
|
id="g190"><path
|
||||||
|
d="m 16.348,14.145 c -0.235,0.246 -0.371,0.628 -0.371,1.125 l 0,-0.118 c 0,-0.496 0.136,-0.879 0.371,-1.125 l 0.058,0.063 -0.058,0.055 z"
|
||||||
|
style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||||
|
id="path192" /></g></g></g></pattern><clipPath
|
||||||
|
clipPathUnits="userSpaceOnUse"
|
||||||
|
id="clipPath206"><path
|
||||||
|
d="M 0,0 124,0 124,48 0,48 0,0 Z"
|
||||||
|
id="path208"
|
||||||
|
inkscape:connector-curvature="0" /></clipPath><mask
|
||||||
|
maskUnits="userSpaceOnUse"
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
width="1"
|
||||||
|
height="1"
|
||||||
|
id="mask210"><g
|
||||||
|
id="g212"><g
|
||||||
|
clip-path="url(#clipPath206)"
|
||||||
|
id="g214"><path
|
||||||
|
d="M 0,0 124,0 124,48 0,48 0,0 Z"
|
||||||
|
style="fill:#000000;fill-opacity:0.12000002;fill-rule:nonzero;stroke:none"
|
||||||
|
id="path216"
|
||||||
|
inkscape:connector-curvature="0" /></g></g></mask><clipPath
|
||||||
|
clipPathUnits="userSpaceOnUse"
|
||||||
|
id="clipPath222"><path
|
||||||
|
d="M 0,0 124,0 124,48 0,48 0,0 Z"
|
||||||
|
id="path224"
|
||||||
|
inkscape:connector-curvature="0" /></clipPath><clipPath
|
||||||
|
clipPathUnits="userSpaceOnUse"
|
||||||
|
id="clipPath226"><path
|
||||||
|
d="M 0,0 124,0 124,48 0,48 0,0 Z"
|
||||||
|
id="path228"
|
||||||
|
inkscape:connector-curvature="0" /></clipPath><pattern
|
||||||
|
patternTransform="matrix(1,0,0,-1,0,48)"
|
||||||
|
patternUnits="userSpaceOnUse"
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
width="124"
|
||||||
|
height="48"
|
||||||
|
id="pattern230"><g
|
||||||
|
id="g232" /><g
|
||||||
|
id="g234"><g
|
||||||
|
clip-path="url(#clipPath226)"
|
||||||
|
id="g236"><g
|
||||||
|
id="g238"><path
|
||||||
|
d="m 33.613,22.961 -3.988,-2.266 0.074,-0.074 3.914,2.223 c 0.559,0.316 0.836,0.734 0.836,1.156 -0.047,-0.379 -0.332,-0.75 -0.836,-1.039 z"
|
||||||
|
style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||||
|
id="path240" /></g></g></g></pattern><clipPath
|
||||||
|
clipPathUnits="userSpaceOnUse"
|
||||||
|
id="clipPath254"><path
|
||||||
|
d="M 0,0 124,0 124,48 0,48 0,0 Z"
|
||||||
|
id="path256"
|
||||||
|
inkscape:connector-curvature="0" /></clipPath><mask
|
||||||
|
maskUnits="userSpaceOnUse"
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
width="1"
|
||||||
|
height="1"
|
||||||
|
id="mask258"><g
|
||||||
|
id="g260"><g
|
||||||
|
clip-path="url(#clipPath254)"
|
||||||
|
id="g262"><path
|
||||||
|
d="M 0,0 124,0 124,48 0,48 0,0 Z"
|
||||||
|
style="fill:#000000;fill-opacity:0.25;fill-rule:nonzero;stroke:none"
|
||||||
|
id="path264"
|
||||||
|
inkscape:connector-curvature="0" /></g></g></mask><clipPath
|
||||||
|
clipPathUnits="userSpaceOnUse"
|
||||||
|
id="clipPath270"><path
|
||||||
|
d="M 0,0 124,0 124,48 0,48 0,0 Z"
|
||||||
|
id="path272"
|
||||||
|
inkscape:connector-curvature="0" /></clipPath><clipPath
|
||||||
|
clipPathUnits="userSpaceOnUse"
|
||||||
|
id="clipPath274"><path
|
||||||
|
d="M 0,0 124,0 124,48 0,48 0,0 Z"
|
||||||
|
id="path276"
|
||||||
|
inkscape:connector-curvature="0" /></clipPath><pattern
|
||||||
|
patternTransform="matrix(1,0,0,-1,0,48)"
|
||||||
|
patternUnits="userSpaceOnUse"
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
width="124"
|
||||||
|
height="48"
|
||||||
|
id="pattern278"><g
|
||||||
|
id="g280" /><g
|
||||||
|
id="g282"><g
|
||||||
|
clip-path="url(#clipPath274)"
|
||||||
|
id="g284"><g
|
||||||
|
id="g286"><path
|
||||||
|
d="m 18.012,33.902 15.601,-8.863 c 0.508,-0.289 0.789,-0.66 0.836,-1.039 0,0.418 -0.277,0.836 -0.836,1.156 L 18.012,34.02 c -1.117,0.632 -2.035,0.105 -2.035,-1.176 l 0,-0.114 c 0,1.278 0.918,1.805 2.035,1.172 z"
|
||||||
|
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||||
|
id="path288" /></g></g></g></pattern></defs><sodipodi:namedview
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1"
|
||||||
|
objecttolerance="10"
|
||||||
|
gridtolerance="10"
|
||||||
|
guidetolerance="10"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:window-width="1366"
|
||||||
|
inkscape:window-height="705"
|
||||||
|
id="namedview4"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:zoom="7.6276974"
|
||||||
|
inkscape:cx="93.965168"
|
||||||
|
inkscape:cy="29.61582"
|
||||||
|
inkscape:window-x="-8"
|
||||||
|
inkscape:window-y="-8"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="g10" /><g
|
||||||
|
id="g10"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
inkscape:label="google-play-badge"
|
||||||
|
transform="matrix(1.25,0,0,-1.25,-9.4247625,49.85025)"><g
|
||||||
|
id="g12"
|
||||||
|
transform="matrix(1.0023923,0,0,0.99072975,-0.29664807,0)"><path
|
||||||
|
d="M 112,8 12,8 C 9.801,8 8,9.801 8,12 l 0,24 c 0,2.199 1.801,4 4,4 l 100,0 c 2.199,0 4,-1.801 4,-4 l 0,-24 c 0,-2.199 -1.801,-4 -4,-4 z"
|
||||||
|
style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||||
|
id="path14"
|
||||||
|
inkscape:connector-curvature="0" /><path
|
||||||
|
d="m 112,39.359 c 1.852,0 3.359,-1.507 3.359,-3.359 l 0,-24 c 0,-1.852 -1.507,-3.359 -3.359,-3.359 l -100,0 c -1.852,0 -3.359,1.507 -3.359,3.359 l 0,24 c 0,1.852 1.507,3.359 3.359,3.359 l 100,0 M 112,40 12,40 C 9.801,40 8,38.199 8,36 L 8,12 C 8,9.801 9.801,8 12,8 l 100,0 c 2.199,0 4,1.801 4,4 l 0,24 c 0,2.199 -1.801,4 -4,4 z"
|
||||||
|
style="fill:#a6a6a6;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||||
|
id="path16"
|
||||||
|
inkscape:connector-curvature="0" /><g
|
||||||
|
id="g18"
|
||||||
|
transform="matrix(1,0,0,-1,0,48)"><path
|
||||||
|
d="m 45.934,16.195 c 0,0.668 -0.2,1.203 -0.594,1.602 -0.453,0.473 -1.043,0.711 -1.766,0.711 -0.691,0 -1.281,-0.242 -1.765,-0.719 -0.485,-0.484 -0.727,-1.078 -0.727,-1.789 0,-0.711 0.242,-1.305 0.727,-1.785 0.484,-0.481 1.074,-0.723 1.765,-0.723 0.344,0 0.672,0.071 0.985,0.203 0.312,0.133 0.566,0.313 0.75,0.535 l -0.418,0.422 c -0.321,-0.379 -0.758,-0.566 -1.317,-0.566 -0.504,0 -0.941,0.176 -1.312,0.531 -0.367,0.356 -0.551,0.817 -0.551,1.383 0,0.566 0.184,1.031 0.551,1.387 0.371,0.351 0.808,0.531 1.312,0.531 0.535,0 0.985,-0.18 1.34,-0.535 0.234,-0.235 0.367,-0.559 0.402,-0.973 l -1.742,0 0,-0.578 2.324,0 c 0.028,0.125 0.036,0.246 0.036,0.363 z"
|
||||||
|
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:0.16;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="path20"
|
||||||
|
inkscape:connector-curvature="0" /></g><g
|
||||||
|
id="g22"
|
||||||
|
transform="matrix(1,0,0,-1,0,48)"><path
|
||||||
|
d="m 49.621,14.191 -2.183,0 0,1.52 1.968,0 0,0.578 -1.968,0 0,1.52 2.183,0 0,0.589 -2.801,0 0,-4.796 2.801,0 0,0.589 z"
|
||||||
|
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:0.16;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="path24"
|
||||||
|
inkscape:connector-curvature="0" /></g><g
|
||||||
|
id="g26"
|
||||||
|
transform="matrix(1,0,0,-1,0,48)"><path
|
||||||
|
d="m 52.223,18.398 -0.618,0 0,-4.207 -1.339,0 0,-0.589 3.297,0 0,0.589 -1.34,0 0,4.207 z"
|
||||||
|
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:0.16;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="path28"
|
||||||
|
inkscape:connector-curvature="0" /></g><g
|
||||||
|
id="g30"
|
||||||
|
transform="matrix(1,0,0,-1,0,48)"><path
|
||||||
|
d="m 55.949,18.398 0,-4.796 0.617,0 0,4.796 -0.617,0 z"
|
||||||
|
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:0.16;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="path32"
|
||||||
|
inkscape:connector-curvature="0" /></g><g
|
||||||
|
id="g34"
|
||||||
|
transform="matrix(1,0,0,-1,0,48)"><path
|
||||||
|
d="m 59.301,18.398 -0.613,0 0,-4.207 -1.344,0 0,-0.589 3.301,0 0,0.589 -1.344,0 0,4.207 z"
|
||||||
|
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:0.16;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="path36"
|
||||||
|
inkscape:connector-curvature="0" /></g><g
|
||||||
|
id="g38"
|
||||||
|
transform="matrix(1,0,0,-1,0,48)"><path
|
||||||
|
d="m 66.887,17.781 c -0.473,0.485 -1.059,0.727 -1.758,0.727 -0.703,0 -1.289,-0.242 -1.762,-0.727 C 62.895,17.297 62.66,16.703 62.66,16 c 0,-0.703 0.235,-1.297 0.707,-1.781 0.473,-0.485 1.059,-0.727 1.762,-0.727 0.695,0 1.281,0.242 1.754,0.731 0.476,0.488 0.711,1.078 0.711,1.777 0,0.703 -0.235,1.297 -0.707,1.781 z m -3.063,-0.402 c 0.356,0.359 0.789,0.539 1.305,0.539 0.512,0 0.949,-0.18 1.301,-0.539 0.355,-0.359 0.535,-0.82 0.535,-1.379 0,-0.559 -0.18,-1.02 -0.535,-1.379 -0.352,-0.359 -0.789,-0.539 -1.301,-0.539 -0.516,0 -0.949,0.18 -1.305,0.539 -0.355,0.359 -0.535,0.82 -0.535,1.379 0,0.559 0.18,1.02 0.535,1.379 z"
|
||||||
|
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:0.16;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="path40"
|
||||||
|
inkscape:connector-curvature="0" /></g><g
|
||||||
|
id="g42"
|
||||||
|
transform="matrix(1,0,0,-1,0,48)"><path
|
||||||
|
d="m 68.461,18.398 0,-4.796 0.75,0 2.332,3.73 0.027,0 -0.027,-0.922 0,-2.808 0.617,0 0,4.796 -0.644,0 -2.442,-3.914 -0.027,0 0.027,0.926 0,2.988 -0.613,0 z"
|
||||||
|
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:0.16;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="path44"
|
||||||
|
inkscape:connector-curvature="0" /></g><path
|
||||||
|
d="m 62.508,22.598 c -1.879,0 -3.414,-1.43 -3.414,-3.403 0,-1.957 1.535,-3.402 3.414,-3.402 1.883,0 3.418,1.445 3.418,3.402 0,1.973 -1.535,3.403 -3.418,3.403 z m 0,-5.465 c -1.031,0 -1.918,0.851 -1.918,2.062 0,1.227 0.887,2.063 1.918,2.063 1.031,0 1.922,-0.836 1.922,-2.063 0,-1.211 -0.891,-2.062 -1.922,-2.062 z m -7.449,5.465 c -1.883,0 -3.414,-1.43 -3.414,-3.403 0,-1.957 1.531,-3.402 3.414,-3.402 1.882,0 3.414,1.445 3.414,3.402 0,1.973 -1.532,3.403 -3.414,3.403 z m 0,-5.465 c -1.032,0 -1.922,0.851 -1.922,2.062 0,1.227 0.89,2.063 1.922,2.063 1.031,0 1.918,-0.836 1.918,-2.063 0,-1.211 -0.887,-2.062 -1.918,-2.062 z m -8.864,4.422 0,-1.446 3.453,0 c -0.101,-0.808 -0.371,-1.402 -0.785,-1.816 -0.504,-0.5 -1.289,-1.055 -2.668,-1.055 -2.125,0 -3.789,1.715 -3.789,3.84 0,2.125 1.664,3.84 3.789,3.84 1.149,0 1.985,-0.449 2.602,-1.031 l 1.019,1.019 c -0.863,0.824 -2.011,1.457 -3.621,1.457 -2.914,0 -5.363,-2.371 -5.363,-5.285 0,-2.914 2.449,-5.285 5.363,-5.285 1.575,0 2.758,0.516 3.688,1.484 0.953,0.953 1.25,2.293 1.25,3.375 0,0.336 -0.028,0.645 -0.078,0.903 l -4.86,0 z m 36.246,-1.121 c -0.281,0.761 -1.148,2.164 -2.914,2.164 -1.75,0 -3.207,-1.379 -3.207,-3.403 0,-1.906 1.442,-3.402 3.375,-3.402 1.563,0 2.465,0.953 2.836,1.508 l -1.16,0.773 c -0.387,-0.566 -0.914,-0.941 -1.676,-0.941 -0.757,0 -1.3,0.347 -1.648,1.031 l 4.551,1.883 -0.157,0.387 z m -4.64,-1.133 c -0.039,1.312 1.019,1.984 1.777,1.984 0.594,0 1.098,-0.297 1.266,-0.722 L 77.801,19.301 Z M 74.102,16 l 1.496,0 0,10 -1.496,0 0,-10 z m -2.45,5.84 -0.05,0 c -0.336,0.398 -0.977,0.758 -1.789,0.758 -1.704,0 -3.262,-1.496 -3.262,-3.414 0,-1.907 1.558,-3.391 3.262,-3.391 0.812,0 1.453,0.363 1.789,0.773 l 0.05,0 0,-0.488 c 0,-1.301 -0.695,-2 -1.816,-2 -0.914,0 -1.481,0.66 -1.715,1.215 L 66.82,14.75 c 0.375,-0.902 1.368,-2.012 3.016,-2.012 1.754,0 3.234,1.032 3.234,3.543 l 0,6.11 -1.418,0 0,-0.551 z m -1.711,-4.707 c -1.031,0 -1.894,0.863 -1.894,2.051 0,1.199 0.863,2.074 1.894,2.074 1.016,0 1.817,-0.875 1.817,-2.074 0,-1.188 -0.801,-2.051 -1.817,-2.051 z M 89.445,26 l -3.578,0 0,-10 1.492,0 0,3.789 2.086,0 c 1.657,0 3.282,1.199 3.282,3.106 0,1.906 -1.629,3.105 -3.282,3.105 z m 0.039,-4.82 -2.125,0 0,3.429 2.125,0 c 1.114,0 1.75,-0.925 1.75,-1.714 0,-0.774 -0.636,-1.715 -1.75,-1.715 z m 9.223,1.437 c -1.078,0 -2.199,-0.476 -2.66,-1.531 l 1.324,-0.555 c 0.285,0.555 0.809,0.735 1.363,0.735 0.774,0 1.559,-0.465 1.571,-1.286 l 0,-0.105 c -0.27,0.156 -0.848,0.387 -1.559,0.387 -1.426,0 -2.879,-0.785 -2.879,-2.25 0,-1.34 1.168,-2.203 2.481,-2.203 1.004,0 1.558,0.453 1.906,0.98 l 0.051,0 0,-0.773 1.441,0 0,3.836 c 0,1.773 -1.324,2.765 -3.039,2.765 z m -0.18,-5.48 c -0.488,0 -1.168,0.242 -1.168,0.847 0,0.774 0.848,1.071 1.582,1.071 0.657,0 0.965,-0.145 1.364,-0.336 -0.117,-0.926 -0.914,-1.582 -1.778,-1.582 z m 8.469,5.261 -1.715,-4.335 -0.051,0 -1.773,4.335 -1.609,0 2.664,-6.058 -1.52,-3.371 1.559,0 4.105,9.429 -1.66,0 z M 93.547,16 l 1.496,0 0,10 -1.496,0 0,-10 z"
|
||||||
|
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||||
|
id="path46"
|
||||||
|
inkscape:connector-curvature="0" /><g
|
||||||
|
id="g48"><path
|
||||||
|
d="M 16.348,33.969 C 16.113,33.723 15.977,33.34 15.977,32.844 l 0,-17.692 c 0,-0.496 0.136,-0.879 0.371,-1.125 l 0.058,-0.054 9.914,9.91 0,0.234 -9.914,9.91 -0.058,-0.058 z"
|
||||||
|
style="fill:url(#linearGradient50);fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||||
|
id="path64"
|
||||||
|
inkscape:connector-curvature="0" /></g><g
|
||||||
|
id="g66"><path
|
||||||
|
d="m 29.621,20.578 -3.301,3.305 0,0.234 3.305,3.305 0.074,-0.043 3.914,-2.227 c 1.117,-0.632 1.117,-1.672 0,-2.308 l -3.914,-2.223 -0.078,-0.043 z"
|
||||||
|
style="fill:url(#linearGradient68);fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||||
|
id="path78"
|
||||||
|
inkscape:connector-curvature="0" /></g><g
|
||||||
|
id="g80"><path
|
||||||
|
d="M 29.699,20.621 26.32,24 16.348,14.027 c 0.371,-0.39 0.976,-0.437 1.664,-0.047 l 11.687,6.641"
|
||||||
|
style="fill:url(#linearGradient82);fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||||
|
id="path88"
|
||||||
|
inkscape:connector-curvature="0" /></g><g
|
||||||
|
id="g90"><path
|
||||||
|
d="M 29.699,27.379 18.012,34.02 c -0.688,0.386 -1.293,0.339 -1.664,-0.051 L 26.32,24 l 3.379,3.379 z"
|
||||||
|
style="fill:url(#linearGradient92);fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||||
|
id="path104"
|
||||||
|
inkscape:connector-curvature="0" /></g><g
|
||||||
|
id="g106"><g
|
||||||
|
id="g108" /><g
|
||||||
|
id="g122"
|
||||||
|
mask="url(#mask114)"><g
|
||||||
|
id="g124" /><g
|
||||||
|
id="g146"><g
|
||||||
|
clip-path="url(#clipPath126)"
|
||||||
|
id="g148"><g
|
||||||
|
id="g150"><path
|
||||||
|
d="M 0,0 124,0 124,48 0,48 0,0 Z"
|
||||||
|
style="fill:url(#pattern134);fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||||
|
id="path152"
|
||||||
|
inkscape:connector-curvature="0" /></g></g></g></g></g><g
|
||||||
|
id="g154"><g
|
||||||
|
id="g156" /><g
|
||||||
|
id="g170"
|
||||||
|
mask="url(#mask162)"><g
|
||||||
|
id="g172" /><g
|
||||||
|
id="g194"><g
|
||||||
|
clip-path="url(#clipPath174)"
|
||||||
|
id="g196"><g
|
||||||
|
id="g198"><path
|
||||||
|
d="M 0,0 124,0 124,48 0,48 0,0 Z"
|
||||||
|
style="fill:url(#pattern182);fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||||
|
id="path200"
|
||||||
|
inkscape:connector-curvature="0" /></g></g></g></g></g><g
|
||||||
|
id="g202"><g
|
||||||
|
id="g204" /><g
|
||||||
|
id="g218"
|
||||||
|
mask="url(#mask210)"><g
|
||||||
|
id="g220" /><g
|
||||||
|
id="g242"><g
|
||||||
|
clip-path="url(#clipPath222)"
|
||||||
|
id="g244"><g
|
||||||
|
id="g246"><path
|
||||||
|
d="M 0,0 124,0 124,48 0,48 0,0 Z"
|
||||||
|
style="fill:url(#pattern230);fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||||
|
id="path248"
|
||||||
|
inkscape:connector-curvature="0" /></g></g></g></g></g><g
|
||||||
|
id="g250"><g
|
||||||
|
id="g252" /><g
|
||||||
|
id="g266"
|
||||||
|
mask="url(#mask258)"><g
|
||||||
|
id="g268" /><g
|
||||||
|
id="g290"><g
|
||||||
|
clip-path="url(#clipPath270)"
|
||||||
|
id="g292"><g
|
||||||
|
id="g294"><path
|
||||||
|
d="M 0,0 124,0 124,48 0,48 0,0 Z"
|
||||||
|
style="fill:url(#pattern278);fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||||
|
id="path296"
|
||||||
|
inkscape:connector-curvature="0" /></g></g></g></g></g></g></g></svg>
|
After Width: | Height: | Size: 22 KiB |
After Width: | Height: | Size: 3.1 KiB |
After Width: | Height: | Size: 2.9 KiB |
After Width: | Height: | Size: 3.9 KiB |
|
@ -0,0 +1,34 @@
|
||||||
|
|
||||||
|
import FirstFactorValidator = require("./lib/firstfactor/FirstFactorValidator");
|
||||||
|
|
||||||
|
import FirstFactor from "./lib/firstfactor/index";
|
||||||
|
import SecondFactor from "./lib/secondfactor/index";
|
||||||
|
import TOTPRegister from "./lib/totp-register/totp-register";
|
||||||
|
import U2fRegister from "./lib/u2f-register/u2f-register";
|
||||||
|
import ResetPasswordRequest from "./lib/reset-password/reset-password-request";
|
||||||
|
import ResetPasswordForm from "./lib/reset-password/reset-password-form";
|
||||||
|
import jslogger = require("js-logger");
|
||||||
|
import jQuery = require("jquery");
|
||||||
|
import Endpoints = require("../../shared/api");
|
||||||
|
|
||||||
|
jslogger.useDefaults();
|
||||||
|
jslogger.setLevel(jslogger.INFO);
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
(<any>window).jQuery = jQuery;
|
||||||
|
require("bootstrap");
|
||||||
|
|
||||||
|
jQuery('[data-toggle="tooltip"]').tooltip();
|
||||||
|
if (window.location.pathname == Endpoints.FIRST_FACTOR_GET)
|
||||||
|
FirstFactor(window, jQuery, FirstFactorValidator, jslogger);
|
||||||
|
else if (window.location.pathname == Endpoints.SECOND_FACTOR_GET)
|
||||||
|
SecondFactor(window, jQuery);
|
||||||
|
else if (window.location.pathname == Endpoints.SECOND_FACTOR_TOTP_IDENTITY_FINISH_GET)
|
||||||
|
TOTPRegister(window, jQuery);
|
||||||
|
else if (window.location.pathname == Endpoints.SECOND_FACTOR_U2F_IDENTITY_FINISH_GET)
|
||||||
|
U2fRegister(window, jQuery);
|
||||||
|
else if (window.location.pathname == Endpoints.RESET_PASSWORD_IDENTITY_FINISH_GET)
|
||||||
|
ResetPasswordForm(window, jQuery);
|
||||||
|
else if (window.location.pathname == Endpoints.RESET_PASSWORD_REQUEST_GET)
|
||||||
|
ResetPasswordRequest(window, jQuery);
|
||||||
|
})();
|
|
@ -0,0 +1,14 @@
|
||||||
|
import BluebirdPromise = require("bluebird");
|
||||||
|
|
||||||
|
export default function ($: JQueryStatic, url: string, data: Object, fn: any,
|
||||||
|
dataType: string): BluebirdPromise<any> {
|
||||||
|
return new BluebirdPromise<any>((resolve, reject) => {
|
||||||
|
$.get(url, {}, undefined, dataType)
|
||||||
|
.done((data: any) => {
|
||||||
|
resolve(data);
|
||||||
|
})
|
||||||
|
.fail((xhr: JQueryXHR, textStatus: string) => {
|
||||||
|
reject(textStatus);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
|
||||||
|
declare type Handler = () => void;
|
||||||
|
|
||||||
|
export interface Handlers {
|
||||||
|
onFadedIn: Handler;
|
||||||
|
onFadedOut: Handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface INotifier {
|
||||||
|
success(msg: string, handlers?: Handlers): void;
|
||||||
|
error(msg: string, handlers?: Handlers): void;
|
||||||
|
warning(msg: string, handlers?: Handlers): void;
|
||||||
|
info(msg: string, handlers?: Handlers): void;
|
||||||
|
}
|
|
@ -0,0 +1,83 @@
|
||||||
|
|
||||||
|
|
||||||
|
import util = require("util");
|
||||||
|
import { INotifier, Handlers } from "./INotifier";
|
||||||
|
|
||||||
|
class NotificationEvent {
|
||||||
|
private element: JQuery;
|
||||||
|
private message: string;
|
||||||
|
private statusType: string;
|
||||||
|
private timeoutId: any;
|
||||||
|
|
||||||
|
constructor(element: JQuery, msg: string, statusType: string) {
|
||||||
|
this.message = msg;
|
||||||
|
this.statusType = statusType;
|
||||||
|
this.element = element;
|
||||||
|
}
|
||||||
|
|
||||||
|
private clearNotification() {
|
||||||
|
this.element.removeClass(this.statusType);
|
||||||
|
this.element.html("");
|
||||||
|
}
|
||||||
|
|
||||||
|
start(handlers?: Handlers) {
|
||||||
|
const that = this;
|
||||||
|
const FADE_TIME = 500;
|
||||||
|
const html = util.format('<i><img src="/img/notifications/%s.png" alt="status %s"/></i>\
|
||||||
|
<span>%s</span>', this.statusType, this.statusType, this.message);
|
||||||
|
this.element.html(html);
|
||||||
|
this.element.addClass(this.statusType);
|
||||||
|
this.element.fadeIn(FADE_TIME, function () {
|
||||||
|
if (handlers)
|
||||||
|
handlers.onFadedIn();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.timeoutId = setTimeout(function () {
|
||||||
|
that.element.fadeOut(FADE_TIME, function () {
|
||||||
|
that.clearNotification();
|
||||||
|
if (handlers)
|
||||||
|
handlers.onFadedOut();
|
||||||
|
});
|
||||||
|
}, 4000);
|
||||||
|
}
|
||||||
|
|
||||||
|
interrupt() {
|
||||||
|
this.clearNotification();
|
||||||
|
this.element.hide();
|
||||||
|
clearTimeout(this.timeoutId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Notifier implements INotifier {
|
||||||
|
private element: JQuery;
|
||||||
|
private onGoingEvent: NotificationEvent;
|
||||||
|
|
||||||
|
constructor(selector: string, $: JQueryStatic) {
|
||||||
|
this.element = $(selector);
|
||||||
|
this.onGoingEvent = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
private displayAndFadeout(msg: string, statusType: string, handlers?: Handlers): void {
|
||||||
|
if (this.onGoingEvent)
|
||||||
|
this.onGoingEvent.interrupt();
|
||||||
|
|
||||||
|
this.onGoingEvent = new NotificationEvent(this.element, msg, statusType);
|
||||||
|
this.onGoingEvent.start(handlers);
|
||||||
|
}
|
||||||
|
|
||||||
|
success(msg: string, handlers?: Handlers) {
|
||||||
|
this.displayAndFadeout(msg, "success", handlers);
|
||||||
|
}
|
||||||
|
|
||||||
|
error(msg: string, handlers?: Handlers) {
|
||||||
|
this.displayAndFadeout(msg, "error", handlers);
|
||||||
|
}
|
||||||
|
|
||||||
|
warning(msg: string, handlers?: Handlers) {
|
||||||
|
this.displayAndFadeout(msg, "warning", handlers);
|
||||||
|
}
|
||||||
|
|
||||||
|
info(msg: string, handlers?: Handlers) {
|
||||||
|
this.displayAndFadeout(msg, "info", handlers);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
|
||||||
|
export class QueryParametersRetriever {
|
||||||
|
static get(name: string, url?: string): string {
|
||||||
|
if (!url) url = window.location.href;
|
||||||
|
name = name.replace(/[\[\]]/g, "\\$&");
|
||||||
|
const regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"),
|
||||||
|
results = regex.exec(url);
|
||||||
|
if (!results) return undefined;
|
||||||
|
if (!results[2]) return "";
|
||||||
|
return decodeURIComponent(results[2].replace(/\+/g, " "));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { BelongToDomain } from "../../../shared/BelongToDomain";
|
||||||
|
|
||||||
|
export function SafeRedirect(url: string, cb: () => void): void {
|
||||||
|
const domain = window.location.hostname.split(".").slice(-2).join(".");
|
||||||
|
if (url.startsWith("/") || BelongToDomain(url, domain)) {
|
||||||
|
window.location.href = url;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cb();
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
|
||||||
|
import BluebirdPromise = require("bluebird");
|
||||||
|
import Endpoints = require("../../../../shared/api");
|
||||||
|
import Constants = require("../../../../shared/constants");
|
||||||
|
import Util = require("util");
|
||||||
|
import UserMessages = require("../../../../shared/UserMessages");
|
||||||
|
|
||||||
|
export function validate(username: string, password: string,
|
||||||
|
keepMeLoggedIn: boolean, redirectUrl: string, $: JQueryStatic)
|
||||||
|
: BluebirdPromise<string> {
|
||||||
|
return new BluebirdPromise<string>(function (resolve, reject) {
|
||||||
|
let url: string;
|
||||||
|
if (redirectUrl != undefined) {
|
||||||
|
const redirectParam = Util.format("%s=%s", Constants.REDIRECT_QUERY_PARAM, redirectUrl);
|
||||||
|
url = Util.format("%s?%s", Endpoints.FIRST_FACTOR_POST, redirectParam);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
url = Util.format("%s", Endpoints.FIRST_FACTOR_POST);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data: any = {
|
||||||
|
username: username,
|
||||||
|
password: password,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (keepMeLoggedIn) {
|
||||||
|
data.keepMeLoggedIn = "true";
|
||||||
|
}
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
method: "POST",
|
||||||
|
url: url,
|
||||||
|
data: data
|
||||||
|
})
|
||||||
|
.done(function (body: any) {
|
||||||
|
if (body && body.error) {
|
||||||
|
reject(new Error(body.error));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve(body.redirect);
|
||||||
|
})
|
||||||
|
.fail(function (xhr: JQueryXHR, textStatus: string) {
|
||||||
|
reject(new Error(UserMessages.AUTHENTICATION_FAILED));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
|
||||||
|
export const USERNAME_FIELD_ID = "#username";
|
||||||
|
export const PASSWORD_FIELD_ID = "#password";
|
||||||
|
export const SIGN_IN_BUTTON_ID = "#signin";
|
||||||
|
export const KEEP_ME_LOGGED_IN_ID = "#keep_me_logged_in";
|
|
@ -0,0 +1,49 @@
|
||||||
|
import FirstFactorValidator = require("./FirstFactorValidator");
|
||||||
|
import JSLogger = require("js-logger");
|
||||||
|
import UISelectors = require("./UISelectors");
|
||||||
|
import { Notifier } from "../Notifier";
|
||||||
|
import { QueryParametersRetriever } from "../QueryParametersRetriever";
|
||||||
|
import Constants = require("../../../../shared/constants");
|
||||||
|
import Endpoints = require("../../../../shared/api");
|
||||||
|
import UserMessages = require("../../../../shared/UserMessages");
|
||||||
|
import { SafeRedirect } from "../SafeRedirect";
|
||||||
|
|
||||||
|
export default function (window: Window, $: JQueryStatic,
|
||||||
|
firstFactorValidator: typeof FirstFactorValidator, jslogger: typeof JSLogger) {
|
||||||
|
|
||||||
|
const notifier = new Notifier(".notification", $);
|
||||||
|
|
||||||
|
function onFormSubmitted() {
|
||||||
|
const username: string = $(UISelectors.USERNAME_FIELD_ID).val() as string;
|
||||||
|
const password: string = $(UISelectors.PASSWORD_FIELD_ID).val() as string;
|
||||||
|
const keepMeLoggedIn: boolean = $(UISelectors.KEEP_ME_LOGGED_IN_ID).is(":checked");
|
||||||
|
|
||||||
|
$("form").css("opacity", 0.5);
|
||||||
|
$("input,button").attr("disabled", "true");
|
||||||
|
$(UISelectors.SIGN_IN_BUTTON_ID).text("Please wait...");
|
||||||
|
|
||||||
|
const redirectUrl = QueryParametersRetriever.get(Constants.REDIRECT_QUERY_PARAM);
|
||||||
|
firstFactorValidator.validate(username, password, keepMeLoggedIn, redirectUrl, $)
|
||||||
|
.then(onFirstFactorSuccess, onFirstFactorFailure);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onFirstFactorSuccess(redirectUrl: string) {
|
||||||
|
SafeRedirect(redirectUrl, () => {
|
||||||
|
notifier.error("Cannot redirect to an external domain.");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onFirstFactorFailure(err: Error) {
|
||||||
|
$("input,button").removeAttr("disabled");
|
||||||
|
$("form").css("opacity", 1);
|
||||||
|
notifier.error(UserMessages.AUTHENTICATION_FAILED);
|
||||||
|
$(UISelectors.PASSWORD_FIELD_ID).select();
|
||||||
|
$(UISelectors.SIGN_IN_BUTTON_ID).text("Sign in");
|
||||||
|
}
|
||||||
|
|
||||||
|
$(window.document).ready(function () {
|
||||||
|
$("form").on("submit", onFormSubmitted);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
|
||||||
|
export const FORM_SELECTOR = ".form-signin";
|
|
@ -0,0 +1,57 @@
|
||||||
|
import BluebirdPromise = require("bluebird");
|
||||||
|
|
||||||
|
import Endpoints = require("../../../../shared/api");
|
||||||
|
import UserMessages = require("../../../../shared/UserMessages");
|
||||||
|
|
||||||
|
import Constants = require("./constants");
|
||||||
|
import { Notifier } from "../Notifier";
|
||||||
|
|
||||||
|
export default function (window: Window, $: JQueryStatic) {
|
||||||
|
const notifier = new Notifier(".notification", $);
|
||||||
|
|
||||||
|
function modifyPassword(newPassword: string) {
|
||||||
|
return new BluebirdPromise(function (resolve, reject) {
|
||||||
|
$.post(Endpoints.RESET_PASSWORD_FORM_POST, {
|
||||||
|
password: newPassword,
|
||||||
|
})
|
||||||
|
.done(function (body: any) {
|
||||||
|
if (body && body.error) {
|
||||||
|
reject(new Error(body.error));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve(body);
|
||||||
|
})
|
||||||
|
.fail(function (xhr, status) {
|
||||||
|
reject(status);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onFormSubmitted() {
|
||||||
|
const password1 = $("#password1").val() as string;
|
||||||
|
const password2 = $("#password2").val() as string;
|
||||||
|
|
||||||
|
if (!password1 || !password2) {
|
||||||
|
notifier.warning(UserMessages.MISSING_PASSWORD);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password1 != password2) {
|
||||||
|
notifier.warning(UserMessages.DIFFERENT_PASSWORDS);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
modifyPassword(password1)
|
||||||
|
.then(function () {
|
||||||
|
window.location.href = Endpoints.FIRST_FACTOR_GET;
|
||||||
|
})
|
||||||
|
.error(function () {
|
||||||
|
notifier.error(UserMessages.RESET_PASSWORD_FAILED);
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$(document).ready(function () {
|
||||||
|
$(Constants.FORM_SELECTOR).on("submit", onFormSubmitted);
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
|
||||||
|
import BluebirdPromise = require("bluebird");
|
||||||
|
|
||||||
|
import Endpoints = require("../../../../shared/api");
|
||||||
|
import UserMessages = require("../../../../shared/UserMessages");
|
||||||
|
import Constants = require("./constants");
|
||||||
|
import jslogger = require("js-logger");
|
||||||
|
import { Notifier } from "../Notifier";
|
||||||
|
|
||||||
|
export default function (window: Window, $: JQueryStatic) {
|
||||||
|
const notifier = new Notifier(".notification", $);
|
||||||
|
|
||||||
|
function requestPasswordReset(username: string) {
|
||||||
|
return new BluebirdPromise(function (resolve, reject) {
|
||||||
|
$.get(Endpoints.RESET_PASSWORD_IDENTITY_START_GET, {
|
||||||
|
userid: username,
|
||||||
|
})
|
||||||
|
.done(function (body: any) {
|
||||||
|
if (body && body.error) {
|
||||||
|
reject(new Error(body.error));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
})
|
||||||
|
.fail(function (xhr: JQueryXHR, textStatus: string) {
|
||||||
|
reject(new Error(textStatus));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onFormSubmitted() {
|
||||||
|
const username = $("#username").val() as string;
|
||||||
|
|
||||||
|
if (!username) {
|
||||||
|
notifier.warning(UserMessages.MISSING_USERNAME);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
requestPasswordReset(username)
|
||||||
|
.then(function () {
|
||||||
|
notifier.success(UserMessages.MAIL_SENT);
|
||||||
|
setTimeout(function () {
|
||||||
|
window.location.replace(Endpoints.FIRST_FACTOR_GET);
|
||||||
|
}, 1000);
|
||||||
|
})
|
||||||
|
.error(function () {
|
||||||
|
notifier.error(UserMessages.MAIL_NOT_SENT);
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$(document).ready(function () {
|
||||||
|
$(Constants.FORM_SELECTOR).on("submit", onFormSubmitted);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
|
||||||
|
import BluebirdPromise = require("bluebird");
|
||||||
|
import Endpoints = require("../../../../shared/api");
|
||||||
|
import { RedirectionMessage } from "../../../../shared/RedirectionMessage";
|
||||||
|
import { ErrorMessage } from "../../../../shared/ErrorMessage";
|
||||||
|
|
||||||
|
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 (body: RedirectionMessage | ErrorMessage) {
|
||||||
|
if (body && "error" in body) {
|
||||||
|
reject(new Error((body as ErrorMessage).error));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve((body as RedirectionMessage).redirect);
|
||||||
|
})
|
||||||
|
.fail(function (xhr: JQueryXHR, textStatus: string) {
|
||||||
|
reject(new Error(textStatus));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
import U2f = require("u2f");
|
||||||
|
import U2fApi from "u2f-api";
|
||||||
|
import BluebirdPromise = require("bluebird");
|
||||||
|
import Endpoints = require("../../../../shared/api");
|
||||||
|
import UserMessages = require("../../../../shared/UserMessages");
|
||||||
|
import { INotifier } from "../INotifier";
|
||||||
|
import { RedirectionMessage } from "../../../../shared/RedirectionMessage";
|
||||||
|
import { ErrorMessage } from "../../../../shared/ErrorMessage";
|
||||||
|
import GetPromised from "../GetPromised";
|
||||||
|
|
||||||
|
function finishU2fAuthentication(responseData: U2fApi.SignResponse,
|
||||||
|
$: JQueryStatic): BluebirdPromise<string> {
|
||||||
|
return new BluebirdPromise<string>(function (resolve, reject) {
|
||||||
|
$.ajax({
|
||||||
|
url: Endpoints.SECOND_FACTOR_U2F_SIGN_POST,
|
||||||
|
data: responseData,
|
||||||
|
method: "POST",
|
||||||
|
dataType: "json"
|
||||||
|
} as JQueryAjaxSettings)
|
||||||
|
.done(function (body: RedirectionMessage | ErrorMessage) {
|
||||||
|
if (body && "error" in body) {
|
||||||
|
reject(new Error((body as ErrorMessage).error));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve((body as RedirectionMessage).redirect);
|
||||||
|
})
|
||||||
|
.fail(function (xhr: JQueryXHR, textStatus: string) {
|
||||||
|
reject(new Error(textStatus));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validate($: JQueryStatic): BluebirdPromise<string> {
|
||||||
|
return GetPromised($, Endpoints.SECOND_FACTOR_U2F_SIGN_REQUEST_GET, {},
|
||||||
|
undefined, "json")
|
||||||
|
.then(function (signRequest: U2f.Request) {
|
||||||
|
return U2fApi.sign(signRequest, 60);
|
||||||
|
})
|
||||||
|
.then(function (signResponse: U2fApi.SignResponse) {
|
||||||
|
return finishU2fAuthentication(signResponse, $);
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
|
||||||
|
export const TOTP_FORM_SELECTOR = ".form-signin.totp";
|
||||||
|
export const TOTP_TOKEN_SELECTOR = ".form-signin #token";
|
|
@ -0,0 +1,59 @@
|
||||||
|
import TOTPValidator = require("./TOTPValidator");
|
||||||
|
import U2FValidator = require("./U2FValidator");
|
||||||
|
import ClientConstants = require("./constants");
|
||||||
|
import { Notifier } from "../Notifier";
|
||||||
|
import { QueryParametersRetriever } from "../QueryParametersRetriever";
|
||||||
|
import UserMessages = require("../../../../shared/UserMessages");
|
||||||
|
import SharedConstants = require("../../../../shared/constants");
|
||||||
|
import { SafeRedirect } from "../SafeRedirect";
|
||||||
|
|
||||||
|
export default function (window: Window, $: JQueryStatic) {
|
||||||
|
const notifier = new Notifier(".notification", $);
|
||||||
|
|
||||||
|
function onAuthenticationSuccess(serverRedirectUrl: string) {
|
||||||
|
const queryRedirectUrl = QueryParametersRetriever.get(SharedConstants.REDIRECT_QUERY_PARAM);
|
||||||
|
if (queryRedirectUrl) {
|
||||||
|
SafeRedirect(queryRedirectUrl, () => {
|
||||||
|
notifier.error(UserMessages.CANNOT_REDIRECT_TO_EXTERNAL_DOMAIN);
|
||||||
|
});
|
||||||
|
} else if (serverRedirectUrl) {
|
||||||
|
SafeRedirect(serverRedirectUrl, () => {
|
||||||
|
notifier.error(UserMessages.CANNOT_REDIRECT_TO_EXTERNAL_DOMAIN);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
notifier.success(UserMessages.AUTHENTICATION_SUCCEEDED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSecondFactorTotpSuccess(redirectUrl: string) {
|
||||||
|
onAuthenticationSuccess(redirectUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSecondFactorTotpFailure(err: Error) {
|
||||||
|
notifier.error(UserMessages.AUTHENTICATION_TOTP_FAILED);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onU2fAuthenticationSuccess(redirectUrl: string) {
|
||||||
|
onAuthenticationSuccess(redirectUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onU2fAuthenticationFailure() {
|
||||||
|
// TODO(clems4ever): we should not display this error message until a device
|
||||||
|
// is registered.
|
||||||
|
// notifier.error(UserMessages.AUTHENTICATION_U2F_FAILED);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onTOTPFormSubmitted(): boolean {
|
||||||
|
const token = $(ClientConstants.TOTP_TOKEN_SELECTOR).val() as string;
|
||||||
|
TOTPValidator.validate(token, $)
|
||||||
|
.then(onSecondFactorTotpSuccess)
|
||||||
|
.catch(onSecondFactorTotpFailure);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$(window.document).ready(function () {
|
||||||
|
$(ClientConstants.TOTP_FORM_SELECTOR).on("submit", onTOTPFormSubmitted);
|
||||||
|
U2FValidator.validate($)
|
||||||
|
.then(onU2fAuthenticationSuccess, onU2fAuthenticationFailure);
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
|
||||||
|
import jslogger = require("js-logger");
|
||||||
|
import UISelector = require("./ui-selector");
|
||||||
|
|
||||||
|
export default function(window: Window, $: JQueryStatic) {
|
||||||
|
jslogger.debug("Creating QRCode from OTPAuth url");
|
||||||
|
const qrcode = $(UISelector.QRCODE_ID_SELECTOR);
|
||||||
|
const val = qrcode.text();
|
||||||
|
qrcode.empty();
|
||||||
|
new (window as any).QRCode(qrcode.get(0), val);
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
|
||||||
|
export const QRCODE_ID_SELECTOR = "#qrcode";
|
|
@ -0,0 +1,56 @@
|
||||||
|
|
||||||
|
import BluebirdPromise = require("bluebird");
|
||||||
|
import U2f = require("u2f");
|
||||||
|
import * as U2fApi from "u2f-api";
|
||||||
|
import { Notifier } from "../Notifier";
|
||||||
|
import GetPromised from "../GetPromised";
|
||||||
|
import Endpoints = require("../../../../shared/api");
|
||||||
|
import UserMessages = require("../../../../shared/UserMessages");
|
||||||
|
import { RedirectionMessage } from "../../../../shared/RedirectionMessage";
|
||||||
|
import { ErrorMessage } from "../../../../shared/ErrorMessage";
|
||||||
|
import { SafeRedirect } from "../SafeRedirect";
|
||||||
|
|
||||||
|
export default function (window: Window, $: JQueryStatic) {
|
||||||
|
const notifier = new Notifier(".notification", $);
|
||||||
|
|
||||||
|
function checkRegistration(regResponse: U2fApi.RegisterResponse): BluebirdPromise<string> {
|
||||||
|
return new BluebirdPromise<string>(function (resolve, reject) {
|
||||||
|
$.post(Endpoints.SECOND_FACTOR_U2F_REGISTER_POST, regResponse, undefined, "json")
|
||||||
|
.done((body: RedirectionMessage | ErrorMessage) => {
|
||||||
|
if (body && "error" in body) {
|
||||||
|
reject(new Error((body as ErrorMessage).error));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve((body as RedirectionMessage).redirect);
|
||||||
|
})
|
||||||
|
.fail((xhr, status) => {
|
||||||
|
reject(new Error("Failed to register device."));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function requestRegistration(): BluebirdPromise<string> {
|
||||||
|
return GetPromised($, Endpoints.SECOND_FACTOR_U2F_REGISTER_REQUEST_GET, {},
|
||||||
|
undefined, "json")
|
||||||
|
.then((registrationRequest: U2f.Request) => {
|
||||||
|
return U2fApi.register(registrationRequest, [], 60);
|
||||||
|
})
|
||||||
|
.then((res) => checkRegistration(res));
|
||||||
|
}
|
||||||
|
|
||||||
|
function onRegisterFailure(err: Error) {
|
||||||
|
notifier.error(UserMessages.REGISTRATION_U2F_FAILED);
|
||||||
|
}
|
||||||
|
|
||||||
|
$(document).ready(function () {
|
||||||
|
requestRegistration()
|
||||||
|
.then((redirectionUrl: string) => {
|
||||||
|
SafeRedirect(redirectionUrl, () => {
|
||||||
|
notifier.error(UserMessages.CANNOT_REDIRECT_TO_EXTERNAL_DOMAIN);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
onRegisterFailure(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,71 @@
|
||||||
|
|
||||||
|
import Assert = require("assert");
|
||||||
|
import Sinon = require("sinon");
|
||||||
|
import JQueryMock = require("./mocks/jquery");
|
||||||
|
|
||||||
|
import { Notifier } from "../src/lib/Notifier";
|
||||||
|
|
||||||
|
describe("test notifier", function() {
|
||||||
|
const SELECTOR = "dummy-selector";
|
||||||
|
const MESSAGE = "This is a message";
|
||||||
|
let jqueryMock: { jquery: JQueryMock.JQueryMock, element: JQueryMock.JQueryElementsMock };
|
||||||
|
let clock: any;
|
||||||
|
|
||||||
|
beforeEach(function() {
|
||||||
|
jqueryMock = JQueryMock.JQueryMock();
|
||||||
|
clock = Sinon.useFakeTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(function() {
|
||||||
|
clock.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
function should_fade_in_and_out_on_notification(notificationType: string): void {
|
||||||
|
const delayReturn = {
|
||||||
|
fadeOut: Sinon.stub()
|
||||||
|
};
|
||||||
|
|
||||||
|
jqueryMock.element.fadeIn.yields();
|
||||||
|
|
||||||
|
function onFadedInCallback() {
|
||||||
|
Assert(jqueryMock.element.fadeIn.calledOnce);
|
||||||
|
Assert(jqueryMock.element.addClass.calledWith(notificationType));
|
||||||
|
Assert(!jqueryMock.element.removeClass.calledWith(notificationType));
|
||||||
|
clock.tick(10 * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onFadedOutCallback() {
|
||||||
|
Assert(jqueryMock.element.removeClass.calledWith(notificationType));
|
||||||
|
Assert(jqueryMock.element.fadeOut.calledOnce);
|
||||||
|
}
|
||||||
|
|
||||||
|
const notifier = new Notifier(SELECTOR, jqueryMock.jquery as any);
|
||||||
|
|
||||||
|
// Call the method by its name... Bad but allows code reuse.
|
||||||
|
(notifier as any)[notificationType](MESSAGE, {
|
||||||
|
onFadedIn: onFadedInCallback,
|
||||||
|
onFadedOut: onFadedOutCallback
|
||||||
|
});
|
||||||
|
|
||||||
|
clock.tick(510);
|
||||||
|
|
||||||
|
Assert(jqueryMock.element.fadeIn.calledOnce);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
it("should fade in and fade out an error message", function() {
|
||||||
|
should_fade_in_and_out_on_notification("error");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fade in and fade out an info message", function() {
|
||||||
|
should_fade_in_and_out_on_notification("info");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fade in and fade out an warning message", function() {
|
||||||
|
should_fade_in_and_out_on_notification("warning");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fade in and fade out an success message", function() {
|
||||||
|
should_fade_in_and_out_on_notification("success");
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,46 @@
|
||||||
|
|
||||||
|
import FirstFactorValidator = require("../../src/lib/firstfactor/FirstFactorValidator");
|
||||||
|
import JQueryMock = require("../mocks/jquery");
|
||||||
|
import BluebirdPromise = require("bluebird");
|
||||||
|
import Assert = require("assert");
|
||||||
|
|
||||||
|
describe("test FirstFactorValidator", function () {
|
||||||
|
it("should validate first factor successfully", () => {
|
||||||
|
const postPromise = JQueryMock.JQueryDeferredMock();
|
||||||
|
postPromise.done.yields({ redirect: "http://redirect" });
|
||||||
|
postPromise.done.returns(postPromise);
|
||||||
|
|
||||||
|
const jqueryMock = JQueryMock.JQueryMock();
|
||||||
|
jqueryMock.jquery.ajax.returns(postPromise);
|
||||||
|
|
||||||
|
return FirstFactorValidator.validate("username", "password", false,
|
||||||
|
"http://redirect", jqueryMock.jquery as any);
|
||||||
|
});
|
||||||
|
|
||||||
|
function should_fail_first_factor_validation(errorMessage: string) {
|
||||||
|
const xhr = {
|
||||||
|
status: 401
|
||||||
|
};
|
||||||
|
const postPromise = JQueryMock.JQueryDeferredMock();
|
||||||
|
postPromise.fail.yields(xhr, errorMessage);
|
||||||
|
postPromise.done.returns(postPromise);
|
||||||
|
|
||||||
|
const jqueryMock = JQueryMock.JQueryMock();
|
||||||
|
jqueryMock.jquery.ajax.returns(postPromise);
|
||||||
|
|
||||||
|
return FirstFactorValidator.validate("username", "password", false,
|
||||||
|
"http://redirect", jqueryMock.jquery as any)
|
||||||
|
.then(function () {
|
||||||
|
return BluebirdPromise.reject(new Error("First factor validation successfully finished while it should have not."));
|
||||||
|
}, function (err: Error) {
|
||||||
|
Assert.equal(errorMessage, err.message);
|
||||||
|
return BluebirdPromise.resolve();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("should fail first factor validation", () => {
|
||||||
|
it("should fail with error", () => {
|
||||||
|
return should_fail_first_factor_validation("Authentication failed. Please check your credentials.");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,33 @@
|
||||||
|
|
||||||
|
import Sinon = require("sinon");
|
||||||
|
import { INotifier } from "../../src/lib/INotifier";
|
||||||
|
|
||||||
|
export class NotifierStub implements INotifier {
|
||||||
|
successStub: Sinon.SinonStub;
|
||||||
|
errorStub: Sinon.SinonStub;
|
||||||
|
warnStub: Sinon.SinonStub;
|
||||||
|
infoStub: Sinon.SinonStub;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.successStub = Sinon.stub();
|
||||||
|
this.errorStub = Sinon.stub();
|
||||||
|
this.warnStub = Sinon.stub();
|
||||||
|
this.infoStub = Sinon.stub();
|
||||||
|
}
|
||||||
|
|
||||||
|
success(msg: string) {
|
||||||
|
this.successStub();
|
||||||
|
}
|
||||||
|
|
||||||
|
error(msg: string) {
|
||||||
|
this.errorStub();
|
||||||
|
}
|
||||||
|
|
||||||
|
warning(msg: string) {
|
||||||
|
this.warnStub();
|
||||||
|
}
|
||||||
|
|
||||||
|
info(msg: string) {
|
||||||
|
this.infoStub();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,59 @@
|
||||||
|
|
||||||
|
import sinon = require("sinon");
|
||||||
|
import jquery = require("jquery");
|
||||||
|
|
||||||
|
|
||||||
|
export interface JQueryMock extends sinon.SinonStub {
|
||||||
|
get: sinon.SinonStub;
|
||||||
|
post: sinon.SinonStub;
|
||||||
|
ajax: sinon.SinonStub;
|
||||||
|
notify: sinon.SinonStub;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface JQueryElementsMock {
|
||||||
|
ready: sinon.SinonStub;
|
||||||
|
show: sinon.SinonStub;
|
||||||
|
hide: sinon.SinonStub;
|
||||||
|
html: sinon.SinonStub;
|
||||||
|
addClass: sinon.SinonStub;
|
||||||
|
removeClass: sinon.SinonStub;
|
||||||
|
fadeIn: sinon.SinonStub;
|
||||||
|
fadeOut: sinon.SinonStub;
|
||||||
|
on: sinon.SinonStub;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface JQueryDeferredMock {
|
||||||
|
done: sinon.SinonStub;
|
||||||
|
fail: sinon.SinonStub;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function JQueryMock(): { jquery: JQueryMock, element: JQueryElementsMock } {
|
||||||
|
const jquery = sinon.stub() as any;
|
||||||
|
const jqueryInstance: JQueryElementsMock = {
|
||||||
|
ready: sinon.stub(),
|
||||||
|
show: sinon.stub(),
|
||||||
|
hide: sinon.stub(),
|
||||||
|
html: sinon.stub(),
|
||||||
|
addClass: sinon.stub(),
|
||||||
|
removeClass: sinon.stub(),
|
||||||
|
fadeIn: sinon.stub(),
|
||||||
|
fadeOut: sinon.stub(),
|
||||||
|
on: sinon.stub()
|
||||||
|
};
|
||||||
|
jquery.ajax = sinon.stub();
|
||||||
|
jquery.get = sinon.stub();
|
||||||
|
jquery.post = sinon.stub();
|
||||||
|
jquery.notify = sinon.stub();
|
||||||
|
jquery.returns(jqueryInstance);
|
||||||
|
return {
|
||||||
|
jquery: jquery,
|
||||||
|
element: jqueryInstance
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function JQueryDeferredMock(): JQueryDeferredMock {
|
||||||
|
return {
|
||||||
|
done: sinon.stub(),
|
||||||
|
fail: sinon.stub()
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
|
||||||
|
import sinon = require("sinon");
|
||||||
|
|
||||||
|
export interface U2FApiMock {
|
||||||
|
sign: sinon.SinonStub;
|
||||||
|
register: sinon.SinonStub;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function U2FApiMock(): U2FApiMock {
|
||||||
|
return {
|
||||||
|
sign: sinon.stub(),
|
||||||
|
register: sinon.stub()
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
|
||||||
|
import TOTPValidator = require("../../src/lib/secondfactor/TOTPValidator");
|
||||||
|
import JQueryMock = require("../mocks/jquery");
|
||||||
|
import BluebirdPromise = require("bluebird");
|
||||||
|
import Assert = require("assert");
|
||||||
|
|
||||||
|
describe("test TOTPValidator", function () {
|
||||||
|
it("should initiate an identity check successfully", () => {
|
||||||
|
const postPromise = JQueryMock.JQueryDeferredMock();
|
||||||
|
postPromise.done.yields({ redirect: "https://home.test.url" });
|
||||||
|
postPromise.done.returns(postPromise);
|
||||||
|
|
||||||
|
const jqueryMock = JQueryMock.JQueryMock();
|
||||||
|
jqueryMock.jquery.ajax.returns(postPromise);
|
||||||
|
|
||||||
|
return TOTPValidator.validate("totp_token", jqueryMock.jquery as any);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fail validating TOTP token", () => {
|
||||||
|
const errorMessage = "Error while validating TOTP token";
|
||||||
|
|
||||||
|
const postPromise = JQueryMock.JQueryDeferredMock();
|
||||||
|
postPromise.fail.yields(undefined, errorMessage);
|
||||||
|
postPromise.done.returns(postPromise);
|
||||||
|
|
||||||
|
const jqueryMock = JQueryMock.JQueryMock();
|
||||||
|
jqueryMock.jquery.ajax.returns(postPromise);
|
||||||
|
|
||||||
|
return TOTPValidator.validate("totp_token", jqueryMock.jquery as any)
|
||||||
|
.then(function () {
|
||||||
|
return BluebirdPromise.reject(new Error("Registration successfully finished while it should have not."));
|
||||||
|
}, function (err: Error) {
|
||||||
|
Assert.equal(errorMessage, err.message);
|
||||||
|
return BluebirdPromise.resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,31 @@
|
||||||
|
|
||||||
|
import sinon = require("sinon");
|
||||||
|
import assert = require("assert");
|
||||||
|
|
||||||
|
import UISelector = require("../../src/lib/totp-register/ui-selector");
|
||||||
|
import TOTPRegister = require("../../src/lib/totp-register/totp-register");
|
||||||
|
|
||||||
|
describe("test totp-register", function() {
|
||||||
|
let jqueryMock: any;
|
||||||
|
let windowMock: any;
|
||||||
|
before(function() {
|
||||||
|
jqueryMock = sinon.stub();
|
||||||
|
windowMock = {
|
||||||
|
QRCode: sinon.spy()
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should create qrcode in page", function() {
|
||||||
|
const mock = {
|
||||||
|
text: sinon.stub(),
|
||||||
|
empty: sinon.stub(),
|
||||||
|
get: sinon.stub()
|
||||||
|
};
|
||||||
|
jqueryMock.withArgs(UISelector.QRCODE_ID_SELECTOR).returns(mock);
|
||||||
|
|
||||||
|
TOTPRegister.default(windowMock, jqueryMock);
|
||||||
|
|
||||||
|
assert(mock.text.calledOnce);
|
||||||
|
assert(mock.empty.calledOnce);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,24 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "commonjs",
|
||||||
|
"target": "es6",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"noImplicitAny": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"removeComments": true,
|
||||||
|
"outDir": "../dist",
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"*": [
|
||||||
|
"./types/*",
|
||||||
|
"../shared/types/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src/**/*"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"test/**/*"
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
{
|
||||||
|
"rules": {
|
||||||
|
"class-name": true,
|
||||||
|
"comment-format": [
|
||||||
|
true,
|
||||||
|
"check-space"
|
||||||
|
],
|
||||||
|
"indent": [
|
||||||
|
true,
|
||||||
|
"spaces"
|
||||||
|
],
|
||||||
|
"one-line": [
|
||||||
|
true,
|
||||||
|
"check-open-brace",
|
||||||
|
"check-whitespace"
|
||||||
|
],
|
||||||
|
"no-var-keyword": true,
|
||||||
|
"quotemark": [
|
||||||
|
true,
|
||||||
|
"double",
|
||||||
|
"avoid-escape"
|
||||||
|
],
|
||||||
|
"semicolon": [
|
||||||
|
true,
|
||||||
|
"always",
|
||||||
|
"ignore-bound-class-methods"
|
||||||
|
],
|
||||||
|
"whitespace": [
|
||||||
|
true,
|
||||||
|
"check-branch",
|
||||||
|
"check-decl",
|
||||||
|
"check-operator",
|
||||||
|
"check-module",
|
||||||
|
"check-separator",
|
||||||
|
"check-type"
|
||||||
|
],
|
||||||
|
"typedef-whitespace": [
|
||||||
|
true,
|
||||||
|
{
|
||||||
|
"call-signature": "nospace",
|
||||||
|
"index-signature": "nospace",
|
||||||
|
"parameter": "nospace",
|
||||||
|
"property-declaration": "nospace",
|
||||||
|
"variable-declaration": "nospace"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"call-signature": "onespace",
|
||||||
|
"index-signature": "onespace",
|
||||||
|
"parameter": "onespace",
|
||||||
|
"property-declaration": "onespace",
|
||||||
|
"variable-declaration": "onespace"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"no-internal-module": true,
|
||||||
|
"no-trailing-whitespace": true,
|
||||||
|
"no-null-keyword": true,
|
||||||
|
"prefer-const": true,
|
||||||
|
"jsdoc-format": true
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
[Dolphin]
|
||||||
|
Timestamp=2018,12,17,20,58,21
|
||||||
|
Version=3
|
||||||
|
ViewMode=1
|
|
@ -0,0 +1,28 @@
|
||||||
|
#! /usr/bin/env node
|
||||||
|
|
||||||
|
import Server from "./lib/Server";
|
||||||
|
import { GlobalDependencies } from "../types/Dependencies";
|
||||||
|
import YAML = require("yamljs");
|
||||||
|
|
||||||
|
const configurationFilepath = process.argv[2];
|
||||||
|
if (!configurationFilepath) {
|
||||||
|
console.log("No config file has been provided.");
|
||||||
|
console.log("Usage: authelia <config>");
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const yamlContent = YAML.load(configurationFilepath);
|
||||||
|
|
||||||
|
const deps: GlobalDependencies = {
|
||||||
|
u2f: require("u2f"),
|
||||||
|
ldapjs: require("ldapjs"),
|
||||||
|
session: require("express-session"),
|
||||||
|
winston: require("winston"),
|
||||||
|
speakeasy: require("speakeasy"),
|
||||||
|
nedb: require("nedb"),
|
||||||
|
ConnectRedis: require("connect-redis"),
|
||||||
|
Redis: require("redis")
|
||||||
|
};
|
||||||
|
|
||||||
|
const server = new Server(deps);
|
||||||
|
server.start(yamlContent, deps);
|
|
@ -0,0 +1,4 @@
|
||||||
|
[Dolphin]
|
||||||
|
Timestamp=2018,12,17,20,59,13
|
||||||
|
Version=3
|
||||||
|
ViewMode=1
|
|
@ -0,0 +1,45 @@
|
||||||
|
|
||||||
|
|
||||||
|
import express = require("express");
|
||||||
|
import U2f = require("u2f");
|
||||||
|
import BluebirdPromise = require("bluebird");
|
||||||
|
import { AuthenticationSession } from "../../types/AuthenticationSession";
|
||||||
|
import { IRequestLogger } from "./logging/IRequestLogger";
|
||||||
|
import { Level } from "./authentication/Level";
|
||||||
|
|
||||||
|
const INITIAL_AUTHENTICATION_SESSION: AuthenticationSession = {
|
||||||
|
keep_me_logged_in: false,
|
||||||
|
authentication_level: Level.NOT_AUTHENTICATED,
|
||||||
|
last_activity_datetime: undefined,
|
||||||
|
userid: undefined,
|
||||||
|
email: undefined,
|
||||||
|
groups: [],
|
||||||
|
register_request: undefined,
|
||||||
|
sign_request: undefined,
|
||||||
|
identity_check: undefined,
|
||||||
|
redirect: undefined
|
||||||
|
};
|
||||||
|
|
||||||
|
export class AuthenticationSessionHandler {
|
||||||
|
static reset(req: express.Request): void {
|
||||||
|
req.session.auth = Object.assign({}, INITIAL_AUTHENTICATION_SESSION, {});
|
||||||
|
|
||||||
|
// Initialize last activity with current time
|
||||||
|
req.session.auth.last_activity_datetime = new Date().getTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
static get(req: express.Request, logger: IRequestLogger): AuthenticationSession {
|
||||||
|
if (!req.session) {
|
||||||
|
const errorMsg = "Something is wrong with session cookies. Please check Redis is running and Authelia can connect to it.";
|
||||||
|
logger.error(req, errorMsg);
|
||||||
|
throw new Error(errorMsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!req.session.auth) {
|
||||||
|
logger.debug(req, "Authentication session %s was undefined. Resetting.", req.sessionID);
|
||||||
|
AuthenticationSessionHandler.reset(req);
|
||||||
|
}
|
||||||
|
|
||||||
|
return req.session.auth;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
import express = require("express");
|
||||||
|
import BluebirdPromise = require("bluebird");
|
||||||
|
import { IRequestLogger } from "./logging/IRequestLogger";
|
||||||
|
|
||||||
|
function replyWithError(req: express.Request, res: express.Response,
|
||||||
|
code: number, logger: IRequestLogger, body?: Object): (err: Error) => void {
|
||||||
|
return function (err: Error): void {
|
||||||
|
if (req.originalUrl.startsWith("/api/") || code == 200) {
|
||||||
|
logger.error(req, "Reply with error %d: %s", code, err.message);
|
||||||
|
logger.debug(req, "%s", err.stack);
|
||||||
|
res.status(code);
|
||||||
|
res.send(body);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
logger.error(req, "Redirect to error %d: %s", code, err.message);
|
||||||
|
logger.debug(req, "%s", err.stack);
|
||||||
|
res.redirect("/error/" + code);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function redirectTo(redirectUrl: string, req: express.Request,
|
||||||
|
res: express.Response, logger: IRequestLogger) {
|
||||||
|
return function(err: Error) {
|
||||||
|
logger.error(req, "Error: %s", err.message);
|
||||||
|
logger.debug(req, "Redirecting to %s", redirectUrl);
|
||||||
|
res.redirect(redirectUrl);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function replyWithError400(req: express.Request,
|
||||||
|
res: express.Response, logger: IRequestLogger) {
|
||||||
|
return replyWithError(req, res, 400, logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function replyWithError401(req: express.Request,
|
||||||
|
res: express.Response, logger: IRequestLogger) {
|
||||||
|
return replyWithError(req, res, 401, logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function replyWithError403(req: express.Request,
|
||||||
|
res: express.Response, logger: IRequestLogger) {
|
||||||
|
return replyWithError(req, res, 403, logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function replyWithError200(req: express.Request,
|
||||||
|
res: express.Response, logger: IRequestLogger, message: string) {
|
||||||
|
return replyWithError(req, res, 200, logger, { error: message });
|
||||||
|
}
|
|
@ -0,0 +1,88 @@
|
||||||
|
|
||||||
|
export class LdapSearchError extends Error {
|
||||||
|
constructor(message?: string) {
|
||||||
|
super(message);
|
||||||
|
this.name = "LdapSearchError";
|
||||||
|
(<any>Object).setPrototypeOf(this, LdapSearchError.prototype);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class LdapBindError extends Error {
|
||||||
|
constructor(message?: string) {
|
||||||
|
super(message);
|
||||||
|
this.name = "LdapBindError";
|
||||||
|
(<any>Object).setPrototypeOf(this, LdapBindError.prototype);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class LdapError extends Error {
|
||||||
|
constructor(message?: string) {
|
||||||
|
super(message);
|
||||||
|
this.name = "LdapError";
|
||||||
|
(<any>Object).setPrototypeOf(this, LdapError.prototype);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class IdentityError extends Error {
|
||||||
|
constructor(message?: string) {
|
||||||
|
super(message);
|
||||||
|
this.name = "IdentityError";
|
||||||
|
(<any>Object).setPrototypeOf(this, IdentityError.prototype);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AccessDeniedError extends Error {
|
||||||
|
constructor(message?: string) {
|
||||||
|
super(message);
|
||||||
|
this.name = "AccessDeniedError";
|
||||||
|
(<any>Object).setPrototypeOf(this, AccessDeniedError.prototype);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AuthenticationRegulationError extends Error {
|
||||||
|
constructor(message?: string) {
|
||||||
|
super(message);
|
||||||
|
this.name = "AuthenticationRegulationError";
|
||||||
|
(<any>Object).setPrototypeOf(this, AuthenticationRegulationError.prototype);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class InvalidTOTPError extends Error {
|
||||||
|
constructor(message?: string) {
|
||||||
|
super(message);
|
||||||
|
this.name = "InvalidTOTPError";
|
||||||
|
(<any>Object).setPrototypeOf(this, InvalidTOTPError.prototype);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class NotAuthenticatedError extends Error {
|
||||||
|
constructor(message?: string) {
|
||||||
|
super(message);
|
||||||
|
this.name = "NotAuthenticatedError";
|
||||||
|
(<any>Object).setPrototypeOf(this, NotAuthenticatedError.prototype);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class NotAuthorizedError extends Error {
|
||||||
|
constructor(message?: string) {
|
||||||
|
super(message);
|
||||||
|
this.name = "NotAuthanticatedError";
|
||||||
|
(<any>Object).setPrototypeOf(this, NotAuthorizedError.prototype);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FirstFactorValidationError extends Error {
|
||||||
|
constructor(message?: string) {
|
||||||
|
super(message);
|
||||||
|
this.name = "FirstFactorValidationError";
|
||||||
|
(<any>Object).setPrototypeOf(this, FirstFactorValidationError.prototype);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SecondFactorValidationError extends Error {
|
||||||
|
constructor(message?: string) {
|
||||||
|
super(message);
|
||||||
|
this.name = "SecondFactorValidationError";
|
||||||
|
(<any>Object).setPrototypeOf(this, FirstFactorValidationError.prototype);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
|
||||||
|
import BluebirdPromise = require("bluebird");
|
||||||
|
import express = require("express");
|
||||||
|
import objectPath = require("object-path");
|
||||||
|
import Exceptions = require("./Exceptions");
|
||||||
|
import { AuthenticationSessionHandler } from "./AuthenticationSessionHandler";
|
||||||
|
import { IRequestLogger } from "./logging/IRequestLogger";
|
||||||
|
import { Level } from "./authentication/Level";
|
||||||
|
|
||||||
|
export function validate(req: express.Request, logger: IRequestLogger): BluebirdPromise<void> {
|
||||||
|
return new BluebirdPromise(function (resolve, reject) {
|
||||||
|
const authSession = AuthenticationSessionHandler.get(req, logger);
|
||||||
|
|
||||||
|
if (!authSession.userid || authSession.authentication_level < Level.ONE_FACTOR)
|
||||||
|
return reject(new Exceptions.FirstFactorValidationError(
|
||||||
|
"First factor has not been validated yet."));
|
||||||
|
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,176 @@
|
||||||
|
|
||||||
|
import sinon = require("sinon");
|
||||||
|
import IdentityValidator = require("./IdentityCheckMiddleware");
|
||||||
|
import { AuthenticationSessionHandler }
|
||||||
|
from "./AuthenticationSessionHandler";
|
||||||
|
import { AuthenticationSession } from "../../types/AuthenticationSession";
|
||||||
|
import { UserDataStore } from "./storage/UserDataStore";
|
||||||
|
import exceptions = require("./Exceptions");
|
||||||
|
import { ServerVariables } from "./ServerVariables";
|
||||||
|
import Assert = require("assert");
|
||||||
|
import express = require("express");
|
||||||
|
import BluebirdPromise = require("bluebird");
|
||||||
|
import ExpressMock = require("./stubs/express.spec");
|
||||||
|
import NotifierMock = require("./notifiers/NotifierStub.spec");
|
||||||
|
import { IdentityValidableStub } from "./IdentityValidableStub.spec";
|
||||||
|
import { RequestLoggerStub } from "./logging/RequestLoggerStub.spec";
|
||||||
|
import { ServerVariablesMock, ServerVariablesMockBuilder }
|
||||||
|
from "./ServerVariablesMockBuilder.spec";
|
||||||
|
import { PRE_VALIDATION_TEMPLATE }
|
||||||
|
from "./IdentityCheckPreValidationTemplate";
|
||||||
|
|
||||||
|
|
||||||
|
describe("IdentityCheckMiddleware", function () {
|
||||||
|
let req: ExpressMock.RequestMock;
|
||||||
|
let res: ExpressMock.ResponseMock;
|
||||||
|
let app: express.Application;
|
||||||
|
let app_get: sinon.SinonStub;
|
||||||
|
let app_post: sinon.SinonStub;
|
||||||
|
let identityValidable: IdentityValidableStub;
|
||||||
|
let mocks: ServerVariablesMock;
|
||||||
|
let vars: ServerVariables;
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
const s = ServerVariablesMockBuilder.build();
|
||||||
|
mocks = s.mocks;
|
||||||
|
vars = s.variables;
|
||||||
|
|
||||||
|
req = ExpressMock.RequestMock();
|
||||||
|
res = ExpressMock.ResponseMock();
|
||||||
|
|
||||||
|
req.headers = {};
|
||||||
|
req.originalUrl = "/non-api/xxx";
|
||||||
|
req.session = {};
|
||||||
|
|
||||||
|
req.query = {};
|
||||||
|
req.app = {};
|
||||||
|
|
||||||
|
identityValidable = new IdentityValidableStub();
|
||||||
|
|
||||||
|
mocks.notifier.notifyStub.returns(BluebirdPromise.resolve());
|
||||||
|
mocks.userDataStore.produceIdentityValidationTokenStub
|
||||||
|
.returns(BluebirdPromise.resolve());
|
||||||
|
mocks.userDataStore.consumeIdentityValidationTokenStub
|
||||||
|
.returns(BluebirdPromise.resolve({ userId: "user" }));
|
||||||
|
|
||||||
|
app = express();
|
||||||
|
app_get = sinon.stub(app, "get");
|
||||||
|
app_post = sinon.stub(app, "post");
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(function () {
|
||||||
|
app_get.restore();
|
||||||
|
app_post.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("test start GET", function () {
|
||||||
|
it("should redirect to error 401 if pre validation initialization \
|
||||||
|
throws a first factor error", function () {
|
||||||
|
identityValidable.preValidationInitStub.returns(BluebirdPromise.reject(
|
||||||
|
new exceptions.FirstFactorValidationError(
|
||||||
|
"Error during prevalidation")));
|
||||||
|
const callback = IdentityValidator.get_start_validation(
|
||||||
|
identityValidable, "/endpoint", vars);
|
||||||
|
|
||||||
|
return callback(req as any, res as any, undefined)
|
||||||
|
.then(() => {
|
||||||
|
Assert(res.redirect.calledWith("/error/401"));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// In that case we answer with 200 to avoid user enumeration.
|
||||||
|
it("should send 200 if email is missing in provided identity", function () {
|
||||||
|
const identity = { userid: "abc" };
|
||||||
|
|
||||||
|
identityValidable.preValidationInitStub
|
||||||
|
.returns(BluebirdPromise.resolve(identity));
|
||||||
|
const callback = IdentityValidator
|
||||||
|
.get_start_validation(identityValidable, "/endpoint", vars);
|
||||||
|
|
||||||
|
return callback(req as any, res as any, undefined)
|
||||||
|
.then(function () {
|
||||||
|
Assert(identityValidable.preValidationResponseStub.called);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// In that case we answer with 200 to avoid user enumeration.
|
||||||
|
it("should send 200 if userid is missing in provided identity",
|
||||||
|
function () {
|
||||||
|
const endpoint = "/protected";
|
||||||
|
const identity = { email: "abc@example.com" };
|
||||||
|
|
||||||
|
identityValidable.preValidationInitStub
|
||||||
|
.returns(BluebirdPromise.resolve(identity));
|
||||||
|
const callback = IdentityValidator
|
||||||
|
.get_start_validation(identityValidable, "/endpoint", vars);
|
||||||
|
|
||||||
|
return callback(req as any, res as any, undefined)
|
||||||
|
.then(function () {
|
||||||
|
Assert(identityValidable.preValidationResponseStub.called);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should issue a token, send an email and return 204", function () {
|
||||||
|
const endpoint = "/protected";
|
||||||
|
const identity = { userid: "user", email: "abc@example.com" };
|
||||||
|
req.get = sinon.stub().withArgs("Host").returns("localhost");
|
||||||
|
|
||||||
|
identityValidable.preValidationInitStub
|
||||||
|
.returns(BluebirdPromise.resolve(identity));
|
||||||
|
const callback = IdentityValidator
|
||||||
|
.get_start_validation(identityValidable, "/finish_endpoint", vars);
|
||||||
|
|
||||||
|
return callback(req as any, res as any, undefined)
|
||||||
|
.then(function () {
|
||||||
|
Assert(mocks.notifier.notifyStub.calledOnce);
|
||||||
|
Assert(mocks.userDataStore.produceIdentityValidationTokenStub
|
||||||
|
.calledOnce);
|
||||||
|
Assert.equal(mocks.userDataStore.produceIdentityValidationTokenStub
|
||||||
|
.getCall(0).args[0], "user");
|
||||||
|
Assert.equal(mocks.userDataStore.produceIdentityValidationTokenStub
|
||||||
|
.getCall(0).args[3], 240000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
describe("test finish GET", function () {
|
||||||
|
it("should send 401 if no identity_token is provided", () => {
|
||||||
|
const callback = IdentityValidator
|
||||||
|
.get_finish_validation(identityValidable, vars);
|
||||||
|
|
||||||
|
return callback(req as any, res as any, undefined)
|
||||||
|
.then(function () {
|
||||||
|
Assert(res.redirect.calledWith("/error/401"));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should call postValidation if identity_token is provided and still \
|
||||||
|
valid", function () {
|
||||||
|
req.query.identity_token = "token";
|
||||||
|
|
||||||
|
const callback = IdentityValidator
|
||||||
|
.get_finish_validation(identityValidable, vars);
|
||||||
|
return callback(req as any, res as any, undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return 401 if identity_token is provided but invalid",
|
||||||
|
function () {
|
||||||
|
req.query.identity_token = "token";
|
||||||
|
|
||||||
|
identityValidable.postValidationInitStub
|
||||||
|
.returns(BluebirdPromise.resolve());
|
||||||
|
mocks.userDataStore.consumeIdentityValidationTokenStub.reset();
|
||||||
|
mocks.userDataStore.consumeIdentityValidationTokenStub
|
||||||
|
.returns(BluebirdPromise.reject(new Error("Invalid token")));
|
||||||
|
|
||||||
|
const callback = IdentityValidator
|
||||||
|
.get_finish_validation(identityValidable, vars);
|
||||||
|
return callback(req as any, res as any, undefined)
|
||||||
|
.then(() => {
|
||||||
|
Assert(res.redirect.calledWith("/error/401"));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,138 @@
|
||||||
|
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 { IUserDataStore } from "./storage/IUserDataStore";
|
||||||
|
import Express = require("express");
|
||||||
|
import ErrorReplies = require("./ErrorReplies");
|
||||||
|
import { AuthenticationSessionHandler } from "./AuthenticationSessionHandler";
|
||||||
|
import { AuthenticationSession } from "../../types/AuthenticationSession";
|
||||||
|
import { ServerVariables } from "./ServerVariables";
|
||||||
|
import { IdentityValidable } from "./IdentityValidable";
|
||||||
|
|
||||||
|
import Identity = require("../../types/Identity");
|
||||||
|
import { IdentityValidationDocument }
|
||||||
|
from "./storage/IdentityValidationDocument";
|
||||||
|
|
||||||
|
const filePath = __dirname + "/../resources/email-template.ejs";
|
||||||
|
const email_template = fs.readFileSync(filePath, "utf8");
|
||||||
|
|
||||||
|
function createAndSaveToken(userid: string, challenge: string,
|
||||||
|
userDataStore: IUserDataStore): BluebirdPromise<string> {
|
||||||
|
|
||||||
|
const five_minutes = 4 * 60 * 1000;
|
||||||
|
const token = randomstring.generate({ length: 64 });
|
||||||
|
const that = this;
|
||||||
|
|
||||||
|
return userDataStore.produceIdentityValidationToken(userid, token, challenge,
|
||||||
|
five_minutes)
|
||||||
|
.then(function () {
|
||||||
|
return BluebirdPromise.resolve(token);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function consumeToken(token: string, challenge: string,
|
||||||
|
userDataStore: IUserDataStore)
|
||||||
|
: BluebirdPromise<IdentityValidationDocument> {
|
||||||
|
return userDataStore.consumeIdentityValidationToken(token, challenge);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function register(app: Express.Application,
|
||||||
|
pre_validation_endpoint: string,
|
||||||
|
post_validation_endpoint: string,
|
||||||
|
handler: IdentityValidable,
|
||||||
|
vars: ServerVariables) {
|
||||||
|
|
||||||
|
app.get(pre_validation_endpoint,
|
||||||
|
get_start_validation(handler, post_validation_endpoint, vars));
|
||||||
|
app.get(post_validation_endpoint,
|
||||||
|
get_finish_validation(handler, vars));
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
vars: ServerVariables)
|
||||||
|
: Express.RequestHandler {
|
||||||
|
|
||||||
|
return function (req: Express.Request, res: Express.Response)
|
||||||
|
: BluebirdPromise<void> {
|
||||||
|
|
||||||
|
let authSession: AuthenticationSession;
|
||||||
|
const identityToken = objectPath.get<Express.Request, string>(
|
||||||
|
req, "query.identity_token");
|
||||||
|
vars.logger.debug(req, "Identity token provided is %s", identityToken);
|
||||||
|
|
||||||
|
return checkIdentityToken(req, identityToken)
|
||||||
|
.then(() => {
|
||||||
|
authSession = AuthenticationSessionHandler.get(req, vars.logger);
|
||||||
|
return handler.postValidationInit(req);
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
return consumeToken(identityToken, handler.challenge(),
|
||||||
|
vars.userDataStore);
|
||||||
|
})
|
||||||
|
.then((doc: IdentityValidationDocument) => {
|
||||||
|
authSession.identity_check = {
|
||||||
|
challenge: handler.challenge(),
|
||||||
|
userid: doc.userId
|
||||||
|
};
|
||||||
|
handler.postValidationResponse(req, res);
|
||||||
|
return BluebirdPromise.resolve();
|
||||||
|
})
|
||||||
|
.catch(ErrorReplies.replyWithError401(req, res, vars.logger));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function get_start_validation(handler: IdentityValidable,
|
||||||
|
postValidationEndpoint: string,
|
||||||
|
vars: ServerVariables)
|
||||||
|
: Express.RequestHandler {
|
||||||
|
return function (req: Express.Request, res: Express.Response)
|
||||||
|
: BluebirdPromise<void> {
|
||||||
|
let identity: Identity.Identity;
|
||||||
|
|
||||||
|
return handler.preValidationInit(req)
|
||||||
|
.then((id: Identity.Identity) => {
|
||||||
|
identity = id;
|
||||||
|
const email = identity.email;
|
||||||
|
const userid = identity.userid;
|
||||||
|
vars.logger.info(req, "Start identity validation of user \"%s\"",
|
||||||
|
userid);
|
||||||
|
|
||||||
|
if (!(email && userid))
|
||||||
|
return BluebirdPromise.reject(new Exceptions.IdentityError(
|
||||||
|
"Missing user id or email address"));
|
||||||
|
|
||||||
|
return createAndSaveToken(userid, handler.challenge(),
|
||||||
|
vars.userDataStore);
|
||||||
|
})
|
||||||
|
.then((token) => {
|
||||||
|
const host = req.get("Host");
|
||||||
|
const link_url = util.format("https://%s%s?identity_token=%s", host,
|
||||||
|
postValidationEndpoint, token);
|
||||||
|
vars.logger.info(req, "Notification sent to user \"%s\"",
|
||||||
|
identity.userid);
|
||||||
|
return vars.notifier.notify(identity.email, handler.mailSubject(),
|
||||||
|
link_url);
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
handler.preValidationResponse(req, res);
|
||||||
|
return BluebirdPromise.resolve();
|
||||||
|
})
|
||||||
|
.catch(Exceptions.IdentityError, (err: Error) => {
|
||||||
|
handler.preValidationResponse(req, res);
|
||||||
|
return BluebirdPromise.resolve();
|
||||||
|
})
|
||||||
|
.catch(ErrorReplies.replyWithError401(req, res, vars.logger));
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
|
||||||
|
|
||||||
|
export const PRE_VALIDATION_TEMPLATE = "need-identity-validation";
|
|
@ -0,0 +1,19 @@
|
||||||
|
import Bluebird = require("bluebird");
|
||||||
|
import Identity = require("../../types/Identity");
|
||||||
|
|
||||||
|
// 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): Bluebird<Identity.Identity>;
|
||||||
|
postValidationInit(req: Express.Request): Bluebird<void>;
|
||||||
|
|
||||||
|
// Serves a page after identity check request
|
||||||
|
preValidationResponse(req: Express.Request, res: Express.Response): void;
|
||||||
|
// Serves the page if identity validated
|
||||||
|
postValidationResponse(req: Express.Request, res: Express.Response): void;
|
||||||
|
mailSubject(): string;
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
|
||||||
|
import Sinon = require("sinon");
|
||||||
|
import { IdentityValidable } from "./IdentityValidable";
|
||||||
|
import express = require("express");
|
||||||
|
import Bluebird = require("bluebird");
|
||||||
|
import { Identity } from "../../types/Identity";
|
||||||
|
|
||||||
|
|
||||||
|
export class IdentityValidableStub implements IdentityValidable {
|
||||||
|
challengeStub: Sinon.SinonStub;
|
||||||
|
preValidationInitStub: Sinon.SinonStub;
|
||||||
|
postValidationInitStub: Sinon.SinonStub;
|
||||||
|
preValidationResponseStub: Sinon.SinonStub;
|
||||||
|
postValidationResponseStub: Sinon.SinonStub;
|
||||||
|
mailSubjectStub: Sinon.SinonStub;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.challengeStub = Sinon.stub();
|
||||||
|
|
||||||
|
this.preValidationInitStub = Sinon.stub();
|
||||||
|
this.postValidationInitStub = Sinon.stub();
|
||||||
|
|
||||||
|
this.preValidationResponseStub = Sinon.stub();
|
||||||
|
this.postValidationResponseStub = Sinon.stub();
|
||||||
|
|
||||||
|
this.mailSubjectStub = Sinon.stub();
|
||||||
|
}
|
||||||
|
|
||||||
|
challenge(): string {
|
||||||
|
return this.challengeStub();
|
||||||
|
}
|
||||||
|
|
||||||
|
preValidationInit(req: Express.Request): Bluebird<Identity> {
|
||||||
|
return this.preValidationInitStub(req);
|
||||||
|
}
|
||||||
|
|
||||||
|
postValidationInit(req: Express.Request): Bluebird<void> {
|
||||||
|
return this.postValidationInitStub(req);
|
||||||
|
}
|
||||||
|
|
||||||
|
preValidationResponse(req: Express.Request, res: Express.Response): void {
|
||||||
|
return this.preValidationResponseStub(req, res);
|
||||||
|
}
|
||||||
|
|
||||||
|
postValidationResponse(req: Express.Request, res: Express.Response): void {
|
||||||
|
return this.postValidationResponseStub(req, res);
|
||||||
|
}
|
||||||
|
|
||||||
|
mailSubject(): string {
|
||||||
|
return this.mailSubjectStub();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,81 @@
|
||||||
|
|
||||||
|
import Assert = require("assert");
|
||||||
|
import Sinon = require("sinon");
|
||||||
|
import nedb = require("nedb");
|
||||||
|
import express = require("express");
|
||||||
|
import winston = require("winston");
|
||||||
|
import speakeasy = require("speakeasy");
|
||||||
|
import u2f = require("u2f");
|
||||||
|
import session = require("express-session");
|
||||||
|
import { Configuration } from "./configuration/schema/Configuration";
|
||||||
|
import { GlobalDependencies } from "../../types/Dependencies";
|
||||||
|
import Server from "./Server";
|
||||||
|
import { LdapjsMock, LdapjsClientMock } from "./stubs/ldapjs.spec";
|
||||||
|
|
||||||
|
|
||||||
|
describe("Server", function () {
|
||||||
|
let deps: GlobalDependencies;
|
||||||
|
let sessionMock: Sinon.SinonSpy;
|
||||||
|
let ldapjsMock: LdapjsMock;
|
||||||
|
|
||||||
|
before(function () {
|
||||||
|
sessionMock = Sinon.spy(session);
|
||||||
|
ldapjsMock = new LdapjsMock();
|
||||||
|
|
||||||
|
deps = {
|
||||||
|
speakeasy: speakeasy,
|
||||||
|
u2f: u2f,
|
||||||
|
nedb: nedb,
|
||||||
|
winston: winston,
|
||||||
|
ldapjs: ldapjsMock as any,
|
||||||
|
session: sessionMock as any,
|
||||||
|
ConnectRedis: Sinon.spy(),
|
||||||
|
Redis: Sinon.spy() as any
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it("should set cookie scope to domain set in the config", function () {
|
||||||
|
const config: Configuration = {
|
||||||
|
port: 8081,
|
||||||
|
session: {
|
||||||
|
domain: "example.com",
|
||||||
|
secret: "secret"
|
||||||
|
},
|
||||||
|
authentication_backend: {
|
||||||
|
ldap: {
|
||||||
|
url: "http://ldap",
|
||||||
|
user: "user",
|
||||||
|
password: "password",
|
||||||
|
base_dn: "dc=example,dc=com"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
notifier: {
|
||||||
|
email: {
|
||||||
|
username: "user@example.com",
|
||||||
|
password: "password",
|
||||||
|
sender: "test@authelia.com",
|
||||||
|
service: "gmail"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
regulation: {
|
||||||
|
max_retries: 3,
|
||||||
|
ban_time: 5 * 60,
|
||||||
|
find_time: 5 * 60
|
||||||
|
},
|
||||||
|
storage: {
|
||||||
|
local: {
|
||||||
|
in_memory: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const server = new Server(deps);
|
||||||
|
server.start(config, deps)
|
||||||
|
.then(function () {
|
||||||
|
Assert(sessionMock.calledOnce);
|
||||||
|
Assert.equal(sessionMock.getCall(0).args[0].cookie.domain, "example.com");
|
||||||
|
server.stop();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,93 @@
|
||||||
|
import BluebirdPromise = require("bluebird");
|
||||||
|
import ObjectPath = require("object-path");
|
||||||
|
|
||||||
|
import { Configuration } from "./configuration/schema/Configuration";
|
||||||
|
import { GlobalDependencies } from "../../types/Dependencies";
|
||||||
|
import { UserDataStore } from "./storage/UserDataStore";
|
||||||
|
import { ConfigurationParser } from "./configuration/ConfigurationParser";
|
||||||
|
import { SessionConfigurationBuilder } from "./configuration/SessionConfigurationBuilder";
|
||||||
|
import { GlobalLogger } from "./logging/GlobalLogger";
|
||||||
|
import { RequestLogger } from "./logging/RequestLogger";
|
||||||
|
import { ServerVariables } from "./ServerVariables";
|
||||||
|
import { ServerVariablesInitializer } from "./ServerVariablesInitializer";
|
||||||
|
import { Configurator } from "./web_server/Configurator";
|
||||||
|
|
||||||
|
import * as Express from "express";
|
||||||
|
import * as Path from "path";
|
||||||
|
import * as http from "http";
|
||||||
|
|
||||||
|
function clone(obj: any) {
|
||||||
|
return JSON.parse(JSON.stringify(obj));
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class Server {
|
||||||
|
private httpServer: http.Server;
|
||||||
|
private globalLogger: GlobalLogger;
|
||||||
|
private requestLogger: RequestLogger;
|
||||||
|
|
||||||
|
constructor(deps: GlobalDependencies) {
|
||||||
|
this.globalLogger = new GlobalLogger(deps.winston);
|
||||||
|
this.requestLogger = new RequestLogger(deps.winston);
|
||||||
|
}
|
||||||
|
|
||||||
|
private displayConfigurations(configuration: Configuration) {
|
||||||
|
const displayableConfiguration: Configuration = clone(configuration);
|
||||||
|
const STARS = "*****";
|
||||||
|
|
||||||
|
if (displayableConfiguration.authentication_backend.ldap) {
|
||||||
|
displayableConfiguration.authentication_backend.ldap.password = STARS;
|
||||||
|
}
|
||||||
|
|
||||||
|
displayableConfiguration.session.secret = STARS;
|
||||||
|
if (displayableConfiguration.notifier && displayableConfiguration.notifier.email)
|
||||||
|
displayableConfiguration.notifier.email.password = STARS;
|
||||||
|
if (displayableConfiguration.notifier && displayableConfiguration.notifier.smtp)
|
||||||
|
displayableConfiguration.notifier.smtp.password = STARS;
|
||||||
|
|
||||||
|
this.globalLogger.debug("User configuration is %s",
|
||||||
|
JSON.stringify(displayableConfiguration, undefined, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
private setup(config: Configuration, app: Express.Application, deps: GlobalDependencies): BluebirdPromise<void> {
|
||||||
|
const that = this;
|
||||||
|
return ServerVariablesInitializer.initialize(
|
||||||
|
config, this.globalLogger, this.requestLogger, deps)
|
||||||
|
.then(function (vars: ServerVariables) {
|
||||||
|
Configurator.configure(config, app, vars, deps);
|
||||||
|
return BluebirdPromise.resolve();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private startServer(app: Express.Application, port: number) {
|
||||||
|
const that = this;
|
||||||
|
that.globalLogger.info("Starting Authelia...");
|
||||||
|
return new BluebirdPromise<void>((resolve, reject) => {
|
||||||
|
this.httpServer = app.listen(port, function (err: string) {
|
||||||
|
that.globalLogger.info("Listening on port %d...", port);
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
start(configuration: Configuration, deps: GlobalDependencies)
|
||||||
|
: BluebirdPromise<void> {
|
||||||
|
const that = this;
|
||||||
|
const app = Express();
|
||||||
|
|
||||||
|
const appConfiguration = ConfigurationParser.parse(configuration);
|
||||||
|
|
||||||
|
// by default the level of logs is info
|
||||||
|
deps.winston.level = appConfiguration.logs_level;
|
||||||
|
this.displayConfigurations(appConfiguration);
|
||||||
|
|
||||||
|
return this.setup(appConfiguration, app, deps)
|
||||||
|
.then(function () {
|
||||||
|
return that.startServer(app, appConfiguration.port);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
this.httpServer.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { IRequestLogger } from "./logging/IRequestLogger";
|
||||||
|
import { ITotpHandler } from "./authentication/totp/ITotpHandler";
|
||||||
|
import { IU2fHandler } from "./authentication/u2f/IU2fHandler";
|
||||||
|
import { IUserDataStore } from "./storage/IUserDataStore";
|
||||||
|
import { INotifier } from "./notifiers/INotifier";
|
||||||
|
import { IRegulator } from "./regulation/IRegulator";
|
||||||
|
import { Configuration } from "./configuration/schema/Configuration";
|
||||||
|
import { IAuthorizer } from "./authorization/IAuthorizer";
|
||||||
|
import { IUsersDatabase } from "./authentication/backends/IUsersDatabase";
|
||||||
|
|
||||||
|
export interface ServerVariables {
|
||||||
|
logger: IRequestLogger;
|
||||||
|
usersDatabase: IUsersDatabase;
|
||||||
|
totpHandler: ITotpHandler;
|
||||||
|
u2f: IU2fHandler;
|
||||||
|
userDataStore: IUserDataStore;
|
||||||
|
notifier: INotifier;
|
||||||
|
regulator: IRegulator;
|
||||||
|
config: Configuration;
|
||||||
|
authorizer: IAuthorizer;
|
||||||
|
}
|
|
@ -0,0 +1,116 @@
|
||||||
|
|
||||||
|
import winston = require("winston");
|
||||||
|
import BluebirdPromise = require("bluebird");
|
||||||
|
import U2F = require("u2f");
|
||||||
|
import Nodemailer = require("nodemailer");
|
||||||
|
|
||||||
|
import { IRequestLogger } from "./logging/IRequestLogger";
|
||||||
|
import { RequestLogger } from "./logging/RequestLogger";
|
||||||
|
|
||||||
|
import { TotpHandler } from "./authentication/totp/TotpHandler";
|
||||||
|
import { ITotpHandler } from "./authentication/totp/ITotpHandler";
|
||||||
|
import { NotifierFactory } from "./notifiers/NotifierFactory";
|
||||||
|
import { MailSenderBuilder } from "./notifiers/MailSenderBuilder";
|
||||||
|
import { LdapUsersDatabase } from "./authentication/backends/ldap/LdapUsersDatabase";
|
||||||
|
import { ConnectorFactory } from "./authentication/backends/ldap/connector/ConnectorFactory";
|
||||||
|
|
||||||
|
import { IUserDataStore } from "./storage/IUserDataStore";
|
||||||
|
import { UserDataStore } from "./storage/UserDataStore";
|
||||||
|
import { INotifier } from "./notifiers/INotifier";
|
||||||
|
import { Regulator } from "./regulation/Regulator";
|
||||||
|
import { IRegulator } from "./regulation/IRegulator";
|
||||||
|
import Configuration = require("./configuration/schema/Configuration");
|
||||||
|
import { CollectionFactoryFactory } from "./storage/CollectionFactoryFactory";
|
||||||
|
import { ICollectionFactory } from "./storage/ICollectionFactory";
|
||||||
|
import { MongoCollectionFactory } from "./storage/mongo/MongoCollectionFactory";
|
||||||
|
import { IMongoClient } from "./connectors/mongo/IMongoClient";
|
||||||
|
|
||||||
|
import { GlobalDependencies } from "../../types/Dependencies";
|
||||||
|
import { ServerVariables } from "./ServerVariables";
|
||||||
|
import { MongoClient } from "./connectors/mongo/MongoClient";
|
||||||
|
import { IGlobalLogger } from "./logging/IGlobalLogger";
|
||||||
|
import { SessionFactory } from "./authentication/backends/ldap/SessionFactory";
|
||||||
|
import { IUsersDatabase } from "./authentication/backends/IUsersDatabase";
|
||||||
|
import { FileUsersDatabase } from "./authentication/backends/file/FileUsersDatabase";
|
||||||
|
import { Authorizer } from "./authorization/Authorizer";
|
||||||
|
|
||||||
|
class UserDataStoreFactory {
|
||||||
|
static create(config: Configuration.Configuration, globalLogger: IGlobalLogger): BluebirdPromise<UserDataStore> {
|
||||||
|
if (config.storage.local) {
|
||||||
|
const nedbOptions: Nedb.DataStoreOptions = {
|
||||||
|
filename: config.storage.local.path,
|
||||||
|
inMemoryOnly: config.storage.local.in_memory
|
||||||
|
};
|
||||||
|
const collectionFactory = CollectionFactoryFactory.createNedb(nedbOptions);
|
||||||
|
return BluebirdPromise.resolve(new UserDataStore(collectionFactory));
|
||||||
|
}
|
||||||
|
else if (config.storage.mongo) {
|
||||||
|
const mongoClient = new MongoClient(
|
||||||
|
config.storage.mongo,
|
||||||
|
globalLogger);
|
||||||
|
const collectionFactory = CollectionFactoryFactory.createMongo(mongoClient);
|
||||||
|
return BluebirdPromise.resolve(new UserDataStore(collectionFactory));
|
||||||
|
}
|
||||||
|
|
||||||
|
return BluebirdPromise.reject(new Error("Storage backend incorrectly configured."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ServerVariablesInitializer {
|
||||||
|
static createUsersDatabase(
|
||||||
|
config: Configuration.Configuration,
|
||||||
|
deps: GlobalDependencies)
|
||||||
|
: IUsersDatabase {
|
||||||
|
|
||||||
|
if (config.authentication_backend.ldap) {
|
||||||
|
const ldapConfig = config.authentication_backend.ldap;
|
||||||
|
return new LdapUsersDatabase(
|
||||||
|
new SessionFactory(
|
||||||
|
ldapConfig,
|
||||||
|
new ConnectorFactory(ldapConfig, deps.ldapjs),
|
||||||
|
deps.winston
|
||||||
|
),
|
||||||
|
ldapConfig
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else if (config.authentication_backend.file) {
|
||||||
|
return new FileUsersDatabase(config.authentication_backend.file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static initialize(
|
||||||
|
config: Configuration.Configuration,
|
||||||
|
globalLogger: IGlobalLogger,
|
||||||
|
requestLogger: IRequestLogger,
|
||||||
|
deps: GlobalDependencies)
|
||||||
|
: BluebirdPromise<ServerVariables> {
|
||||||
|
|
||||||
|
const mailSenderBuilder =
|
||||||
|
new MailSenderBuilder(Nodemailer);
|
||||||
|
const notifier = NotifierFactory.build(
|
||||||
|
config.notifier, mailSenderBuilder);
|
||||||
|
const authorizer = new Authorizer(config.access_control, deps.winston);
|
||||||
|
const totpHandler = new TotpHandler(deps.speakeasy);
|
||||||
|
const usersDatabase = this.createUsersDatabase(
|
||||||
|
config, deps);
|
||||||
|
|
||||||
|
return UserDataStoreFactory.create(config, globalLogger)
|
||||||
|
.then(function (userDataStore: UserDataStore) {
|
||||||
|
const regulator = new Regulator(userDataStore, config.regulation.max_retries,
|
||||||
|
config.regulation.find_time, config.regulation.ban_time);
|
||||||
|
|
||||||
|
const variables: ServerVariables = {
|
||||||
|
authorizer: authorizer,
|
||||||
|
config: config,
|
||||||
|
usersDatabase: usersDatabase,
|
||||||
|
logger: requestLogger,
|
||||||
|
notifier: notifier,
|
||||||
|
regulator: regulator,
|
||||||
|
totpHandler: totpHandler,
|
||||||
|
u2f: deps.u2f,
|
||||||
|
userDataStore: userDataStore
|
||||||
|
};
|
||||||
|
return BluebirdPromise.resolve(variables);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,87 @@
|
||||||
|
import { ServerVariables } from "./ServerVariables";
|
||||||
|
|
||||||
|
import { Configuration } from "./configuration/schema/Configuration";
|
||||||
|
import { IUsersDatabaseStub } from "./authentication/backends/IUsersDatabaseStub.spec";
|
||||||
|
import { AuthorizerStub } from "./authorization/AuthorizerStub.spec";
|
||||||
|
import { RequestLoggerStub } from "./logging/RequestLoggerStub.spec";
|
||||||
|
import { NotifierStub } from "./notifiers/NotifierStub.spec";
|
||||||
|
import { RegulatorStub } from "./regulation/RegulatorStub.spec";
|
||||||
|
import { TotpHandlerStub } from "./authentication/totp/TotpHandlerStub.spec";
|
||||||
|
import { UserDataStoreStub } from "./storage/UserDataStoreStub.spec";
|
||||||
|
import { U2fHandlerStub } from "./authentication/u2f/U2fHandlerStub.spec";
|
||||||
|
|
||||||
|
export interface ServerVariablesMock {
|
||||||
|
authorizer: AuthorizerStub;
|
||||||
|
config: Configuration;
|
||||||
|
usersDatabase: IUsersDatabaseStub;
|
||||||
|
logger: RequestLoggerStub;
|
||||||
|
notifier: NotifierStub;
|
||||||
|
regulator: RegulatorStub;
|
||||||
|
totpHandler: TotpHandlerStub;
|
||||||
|
userDataStore: UserDataStoreStub;
|
||||||
|
u2f: U2fHandlerStub;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ServerVariablesMockBuilder {
|
||||||
|
static build(enableLogging?: boolean): { variables: ServerVariables, mocks: ServerVariablesMock} {
|
||||||
|
const mocks: ServerVariablesMock = {
|
||||||
|
authorizer: new AuthorizerStub(),
|
||||||
|
config: {
|
||||||
|
access_control: {},
|
||||||
|
totp: {
|
||||||
|
issuer: "authelia.com"
|
||||||
|
},
|
||||||
|
authentication_backend: {
|
||||||
|
ldap: {
|
||||||
|
url: "ldap://ldap",
|
||||||
|
base_dn: "dc=example,dc=com",
|
||||||
|
user: "user",
|
||||||
|
password: "password",
|
||||||
|
mail_attribute: "mail",
|
||||||
|
additional_users_dn: "ou=users",
|
||||||
|
additional_groups_dn: "ou=groups",
|
||||||
|
users_filter: "cn={0}",
|
||||||
|
groups_filter: "member={dn}",
|
||||||
|
group_name_attribute: "cn"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
logs_level: "debug",
|
||||||
|
notifier: {},
|
||||||
|
port: 8080,
|
||||||
|
regulation: {
|
||||||
|
ban_time: 50,
|
||||||
|
find_time: 50,
|
||||||
|
max_retries: 3
|
||||||
|
},
|
||||||
|
session: {
|
||||||
|
secret: "my_secret",
|
||||||
|
domain: "mydomain"
|
||||||
|
},
|
||||||
|
storage: {}
|
||||||
|
},
|
||||||
|
usersDatabase: new IUsersDatabaseStub(),
|
||||||
|
logger: new RequestLoggerStub(enableLogging),
|
||||||
|
notifier: new NotifierStub(),
|
||||||
|
regulator: new RegulatorStub(),
|
||||||
|
totpHandler: new TotpHandlerStub(),
|
||||||
|
userDataStore: new UserDataStoreStub(),
|
||||||
|
u2f: new U2fHandlerStub()
|
||||||
|
};
|
||||||
|
const vars: ServerVariables = {
|
||||||
|
authorizer: mocks.authorizer,
|
||||||
|
config: mocks.config,
|
||||||
|
usersDatabase: mocks.usersDatabase,
|
||||||
|
logger: mocks.logger,
|
||||||
|
notifier: mocks.notifier,
|
||||||
|
regulator: mocks.regulator,
|
||||||
|
totpHandler: mocks.totpHandler,
|
||||||
|
userDataStore: mocks.userDataStore,
|
||||||
|
u2f: mocks.u2f
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
variables: vars,
|
||||||
|
mocks: mocks
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
export enum Level {
|
||||||
|
NOT_AUTHENTICATED = 0,
|
||||||
|
ONE_FACTOR = 1,
|
||||||
|
TWO_FACTOR = 2
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
|
||||||
|
export interface GroupsAndEmails {
|
||||||
|
groups: string[];
|
||||||
|
emails: string[];
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
import Bluebird = require("bluebird");
|
||||||
|
|
||||||
|
import { GroupsAndEmails } from "./GroupsAndEmails";
|
||||||
|
|
||||||
|
export interface IUsersDatabase {
|
||||||
|
checkUserPassword(username: string, password: string): Bluebird<GroupsAndEmails>;
|
||||||
|
getEmails(username: string): Bluebird<string[]>;
|
||||||
|
getGroups(username: string): Bluebird<string[]>;
|
||||||
|
updatePassword(username: string, newPassword: string): Bluebird<void>;
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
import Bluebird = require("bluebird");
|
||||||
|
import Sinon = require("sinon");
|
||||||
|
|
||||||
|
import { IUsersDatabase } from "./IUsersDatabase";
|
||||||
|
import { GroupsAndEmails } from "./GroupsAndEmails";
|
||||||
|
|
||||||
|
export class IUsersDatabaseStub implements IUsersDatabase {
|
||||||
|
checkUserPasswordStub: Sinon.SinonStub;
|
||||||
|
getEmailsStub: Sinon.SinonStub;
|
||||||
|
getGroupsStub: Sinon.SinonStub;
|
||||||
|
updatePasswordStub: Sinon.SinonStub;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.checkUserPasswordStub = Sinon.stub();
|
||||||
|
this.getEmailsStub = Sinon.stub();
|
||||||
|
this.getGroupsStub = Sinon.stub();
|
||||||
|
this.updatePasswordStub = Sinon.stub();
|
||||||
|
}
|
||||||
|
|
||||||
|
checkUserPassword(username: string, password: string): Bluebird<GroupsAndEmails> {
|
||||||
|
return this.checkUserPasswordStub(username, password);
|
||||||
|
}
|
||||||
|
|
||||||
|
getEmails(username: string): Bluebird<string[]> {
|
||||||
|
return this.getEmailsStub(username);
|
||||||
|
}
|
||||||
|
|
||||||
|
getGroups(username: string): Bluebird<string[]> {
|
||||||
|
return this.getGroupsStub(username);
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePassword(username: string, newPassword: string): Bluebird<void> {
|
||||||
|
return this.updatePasswordStub(username, newPassword);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,224 @@
|
||||||
|
import Assert = require("assert");
|
||||||
|
import Bluebird = require("bluebird");
|
||||||
|
import Fs = require("fs");
|
||||||
|
import Sinon = require("sinon");
|
||||||
|
import Tmp = require("tmp");
|
||||||
|
|
||||||
|
import { FileUsersDatabase } from "./FileUsersDatabase";
|
||||||
|
import { FileUsersDatabaseConfiguration } from "../../../configuration/schema/FileUsersDatabaseConfiguration";
|
||||||
|
import { HashGenerator } from "../../../utils/HashGenerator";
|
||||||
|
|
||||||
|
const GOOD_DATABASE = `
|
||||||
|
users:
|
||||||
|
john:
|
||||||
|
password: "{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/"
|
||||||
|
email: john.doe@authelia.com
|
||||||
|
groups:
|
||||||
|
- admins
|
||||||
|
- dev
|
||||||
|
|
||||||
|
harry:
|
||||||
|
password: "{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/"
|
||||||
|
emails: harry.potter@authelia.com
|
||||||
|
groups: []
|
||||||
|
`;
|
||||||
|
|
||||||
|
const BAD_HASH = `
|
||||||
|
users:
|
||||||
|
john:
|
||||||
|
password: "{CRYPT}$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/"
|
||||||
|
email: john.doe@authelia.com
|
||||||
|
groups:
|
||||||
|
- admins
|
||||||
|
- dev
|
||||||
|
`;
|
||||||
|
|
||||||
|
const NO_PASSWORD_DATABASE = `
|
||||||
|
users:
|
||||||
|
john:
|
||||||
|
email: john.doe@authelia.com
|
||||||
|
groups:
|
||||||
|
- admins
|
||||||
|
- dev
|
||||||
|
`;
|
||||||
|
|
||||||
|
const NO_EMAIL_DATABASE = `
|
||||||
|
users:
|
||||||
|
john:
|
||||||
|
password: "{CRYPT}$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/"
|
||||||
|
groups:
|
||||||
|
- admins
|
||||||
|
- dev
|
||||||
|
`;
|
||||||
|
|
||||||
|
const SINGLE_USER_DATABASE = `
|
||||||
|
users:
|
||||||
|
john:
|
||||||
|
password: "{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/"
|
||||||
|
email: john.doe@authelia.com
|
||||||
|
groups:
|
||||||
|
- admins
|
||||||
|
- dev
|
||||||
|
`
|
||||||
|
|
||||||
|
function createTmpFileFrom(yaml: string) {
|
||||||
|
const tmpFileAsync = Bluebird.promisify(Tmp.file);
|
||||||
|
return tmpFileAsync()
|
||||||
|
.then((path: string) => {
|
||||||
|
Fs.writeFileSync(path, yaml, "utf-8");
|
||||||
|
return Bluebird.resolve(path);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("authentication/backends/file/FileUsersDatabase", function() {
|
||||||
|
let configuration: FileUsersDatabaseConfiguration;
|
||||||
|
|
||||||
|
describe("checkUserPassword", () => {
|
||||||
|
describe("good config", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
return createTmpFileFrom(GOOD_DATABASE)
|
||||||
|
.then((path: string) => configuration = {
|
||||||
|
path: path
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should succeed", () => {
|
||||||
|
const usersDatabase = new FileUsersDatabase(configuration);
|
||||||
|
return usersDatabase.checkUserPassword("john", "password")
|
||||||
|
.then((groupsAndEmails) => {
|
||||||
|
Assert.deepEqual(groupsAndEmails.groups, ["admins", "dev"]);
|
||||||
|
Assert.deepEqual(groupsAndEmails.emails, ["john.doe@authelia.com"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fail when password is wrong", () => {
|
||||||
|
const usersDatabase = new FileUsersDatabase(configuration);
|
||||||
|
return usersDatabase.checkUserPassword("john", "bad_password")
|
||||||
|
.then(() => Bluebird.reject(new Error("should not be here.")))
|
||||||
|
.catch((err) => {
|
||||||
|
return Bluebird.resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fail when user does not exist", () => {
|
||||||
|
const usersDatabase = new FileUsersDatabase(configuration);
|
||||||
|
return usersDatabase.checkUserPassword("no_user", "password")
|
||||||
|
.then(() => Bluebird.reject(new Error("should not be here.")))
|
||||||
|
.catch((err) => {
|
||||||
|
return Bluebird.resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("bad hash", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
return createTmpFileFrom(GOOD_DATABASE)
|
||||||
|
.then((path: string) => configuration = {
|
||||||
|
path: path
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fail when hash is wrong", () => {
|
||||||
|
const usersDatabase = new FileUsersDatabase(configuration);
|
||||||
|
return usersDatabase.checkUserPassword("john", "password")
|
||||||
|
.then(() => Bluebird.reject(new Error("should not be here.")))
|
||||||
|
.catch((err) => {
|
||||||
|
return Bluebird.resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("no password", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
return createTmpFileFrom(NO_PASSWORD_DATABASE)
|
||||||
|
.then((path: string) => configuration = {
|
||||||
|
path: path
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fail", () => {
|
||||||
|
const usersDatabase = new FileUsersDatabase(configuration);
|
||||||
|
return usersDatabase.checkUserPassword("john", "password")
|
||||||
|
.then(() => Bluebird.reject(new Error("should not be here.")))
|
||||||
|
.catch((err) => {
|
||||||
|
return Bluebird.resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getEmails", () => {
|
||||||
|
describe("good config", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
return createTmpFileFrom(GOOD_DATABASE)
|
||||||
|
.then((path: string) => configuration = {
|
||||||
|
path: path
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should succeed", () => {
|
||||||
|
const usersDatabase = new FileUsersDatabase(configuration);
|
||||||
|
return usersDatabase.getEmails("john")
|
||||||
|
.then((emails) => {
|
||||||
|
Assert.deepEqual(emails, ["john.doe@authelia.com"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fail when user does not exist", () => {
|
||||||
|
const usersDatabase = new FileUsersDatabase(configuration);
|
||||||
|
return usersDatabase.getEmails("no_user")
|
||||||
|
.then(() => Bluebird.reject(new Error("should not be here.")))
|
||||||
|
.catch((err) => {
|
||||||
|
return Bluebird.resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("no email provided", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
return createTmpFileFrom(NO_EMAIL_DATABASE)
|
||||||
|
.then((path: string) => configuration = {
|
||||||
|
path: path
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fail", () => {
|
||||||
|
const usersDatabase = new FileUsersDatabase(configuration);
|
||||||
|
return usersDatabase.getEmails("john")
|
||||||
|
.then(() => Bluebird.reject(new Error("should not be here.")))
|
||||||
|
.catch((err) => {
|
||||||
|
return Bluebird.resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("updatePassword", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
return createTmpFileFrom(SINGLE_USER_DATABASE)
|
||||||
|
.then((path: string) => configuration = {
|
||||||
|
path: path
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should succeed", () => {
|
||||||
|
const usersDatabase = new FileUsersDatabase(configuration);
|
||||||
|
const NEW_HASH = "{CRYPT}$6$rounds=500000$Qw6MhgADvLyYMEq9$ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||||
|
const stub = Sinon.stub(HashGenerator, "ssha512").returns(Bluebird.resolve(NEW_HASH));
|
||||||
|
return usersDatabase.updatePassword("john", "mypassword")
|
||||||
|
.then(() => {
|
||||||
|
const content = Fs.readFileSync(configuration.path, "utf-8");
|
||||||
|
const matches = content.match(/password: '(.+)'/);
|
||||||
|
Assert.equal(matches[1], NEW_HASH);
|
||||||
|
})
|
||||||
|
.finally(() => stub.restore());
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fail when user does not exist", () => {
|
||||||
|
const usersDatabase = new FileUsersDatabase(configuration);
|
||||||
|
return usersDatabase.updatePassword("bad_user", "mypassword")
|
||||||
|
.then(() => Bluebird.reject(new Error("should not be here")))
|
||||||
|
.catch(() => Bluebird.resolve());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,182 @@
|
||||||
|
import Bluebird = require("bluebird");
|
||||||
|
import Fs = require("fs");
|
||||||
|
import Yaml = require("yamljs");
|
||||||
|
|
||||||
|
import { FileUsersDatabaseConfiguration }
|
||||||
|
from "../../../configuration/schema/FileUsersDatabaseConfiguration";
|
||||||
|
import { GroupsAndEmails } from "../GroupsAndEmails";
|
||||||
|
import { IUsersDatabase } from "../IUsersDatabase";
|
||||||
|
import { HashGenerator } from "../../../utils/HashGenerator";
|
||||||
|
import { ReadWriteQueue } from "./ReadWriteQueue";
|
||||||
|
|
||||||
|
const loadAsync = Bluebird.promisify(Yaml.load);
|
||||||
|
|
||||||
|
export class FileUsersDatabase implements IUsersDatabase {
|
||||||
|
private configuration: FileUsersDatabaseConfiguration;
|
||||||
|
private queue: ReadWriteQueue;
|
||||||
|
|
||||||
|
constructor(configuration: FileUsersDatabaseConfiguration) {
|
||||||
|
this.configuration = configuration;
|
||||||
|
this.queue = new ReadWriteQueue(this.configuration.path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read database from file.
|
||||||
|
* It enqueues the read task so that it is scheduled
|
||||||
|
* between other reads and writes.
|
||||||
|
*/
|
||||||
|
private readDatabase(): Bluebird<any> {
|
||||||
|
return new Bluebird<string>((resolve, reject) => {
|
||||||
|
this.queue.read((err: Error, data: string) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve(data);
|
||||||
|
this.queue.next();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then((content) => {
|
||||||
|
const database = Yaml.parse(content);
|
||||||
|
if (!database) {
|
||||||
|
return Bluebird.reject(new Error("Unable to parse YAML file."));
|
||||||
|
}
|
||||||
|
return Bluebird.resolve(database);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks the user exists in the database.
|
||||||
|
*/
|
||||||
|
private checkUserExists(
|
||||||
|
database: any,
|
||||||
|
username: string)
|
||||||
|
: Bluebird<void> {
|
||||||
|
if (!(username in database.users)) {
|
||||||
|
return Bluebird.reject(
|
||||||
|
new Error(`User ${username} does not exist in database.`));
|
||||||
|
}
|
||||||
|
return Bluebird.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check the password of a given user.
|
||||||
|
*/
|
||||||
|
private checkPassword(
|
||||||
|
database: any,
|
||||||
|
username: string,
|
||||||
|
password: string)
|
||||||
|
: Bluebird<void> {
|
||||||
|
const storedHash: string = database.users[username].password;
|
||||||
|
const matches = storedHash.match(/rounds=([0-9]+)\$([a-zA-z0-9]+)\$/);
|
||||||
|
if (!(matches && matches.length == 3)) {
|
||||||
|
return Bluebird.reject(new Error("Unable to detect the hash salt and rounds. " +
|
||||||
|
"Make sure the password is hashed with SSHA512."));
|
||||||
|
}
|
||||||
|
|
||||||
|
const rounds: number = parseInt(matches[1]);
|
||||||
|
const salt = matches[2];
|
||||||
|
|
||||||
|
return HashGenerator.ssha512(password, rounds, salt)
|
||||||
|
.then((hash: string) => {
|
||||||
|
if (hash !== storedHash) {
|
||||||
|
return Bluebird.reject(new Error("Wrong username/password."));
|
||||||
|
}
|
||||||
|
return Bluebird.resolve();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve email addresses of a given user.
|
||||||
|
*/
|
||||||
|
private retrieveEmails(
|
||||||
|
database: any,
|
||||||
|
username: string)
|
||||||
|
: Bluebird<string[]> {
|
||||||
|
if (!("email" in database.users[username])) {
|
||||||
|
return Bluebird.reject(
|
||||||
|
new Error(`User ${username} has no email address.`));
|
||||||
|
}
|
||||||
|
return Bluebird.resolve(
|
||||||
|
[database.users[username].email]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private retrieveGroups(
|
||||||
|
database: any,
|
||||||
|
username: string)
|
||||||
|
: Bluebird<string[]> {
|
||||||
|
if (!("groups" in database.users[username])) {
|
||||||
|
return Bluebird.resolve([]);
|
||||||
|
}
|
||||||
|
return Bluebird.resolve(
|
||||||
|
database.users[username].groups);
|
||||||
|
}
|
||||||
|
|
||||||
|
private replacePassword(
|
||||||
|
database: any,
|
||||||
|
username: string,
|
||||||
|
newPassword: string)
|
||||||
|
: Bluebird<void> {
|
||||||
|
const that = this;
|
||||||
|
return HashGenerator.ssha512(newPassword)
|
||||||
|
.then((hash) => {
|
||||||
|
database.users[username].password = hash;
|
||||||
|
const str = Yaml.stringify(database, 4, 2);
|
||||||
|
return Bluebird.resolve(str);
|
||||||
|
})
|
||||||
|
.then((content: string) => {
|
||||||
|
return new Bluebird((resolve, reject) => {
|
||||||
|
that.queue.write(content, (err) => {
|
||||||
|
if (err) {
|
||||||
|
return reject(err);
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
that.queue.next();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
checkUserPassword(
|
||||||
|
username: string,
|
||||||
|
password: string)
|
||||||
|
: Bluebird<GroupsAndEmails> {
|
||||||
|
return this.readDatabase()
|
||||||
|
.then((database) => {
|
||||||
|
return this.checkUserExists(database, username)
|
||||||
|
.then(() => this.checkPassword(database, username, password))
|
||||||
|
.then(() => {
|
||||||
|
return Bluebird.join(
|
||||||
|
this.retrieveEmails(database, username),
|
||||||
|
this.retrieveGroups(database, username)
|
||||||
|
).spread((emails: string[], groups: string[]) => {
|
||||||
|
return { emails: emails, groups: groups };
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getEmails(username: string): Bluebird<string[]> {
|
||||||
|
return this.readDatabase()
|
||||||
|
.then((database) => {
|
||||||
|
return this.checkUserExists(database, username)
|
||||||
|
.then(() => this.retrieveEmails(database, username));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getGroups(username: string): Bluebird<string[]> {
|
||||||
|
return this.readDatabase()
|
||||||
|
.then((database) => {
|
||||||
|
return this.checkUserExists(database, username)
|
||||||
|
.then(() => this.retrieveGroups(database, username));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePassword(username: string, newPassword: string): Bluebird<void> {
|
||||||
|
return this.readDatabase()
|
||||||
|
.then((database) => {
|
||||||
|
return this.checkUserExists(database, username)
|
||||||
|
.then(() => this.replacePassword(database, username, newPassword));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
import Fs = require("fs");
|
||||||
|
|
||||||
|
type Callback = (err: Error, data?: string) => void;
|
||||||
|
type ContentAndCallback = [string, Callback] | [string, string, Callback];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WriteQueue is a queue synchronizing writes to a file.
|
||||||
|
*
|
||||||
|
* Example of use:
|
||||||
|
*
|
||||||
|
* queue.add(mycontent, (err) => {
|
||||||
|
* // do whatever you want here.
|
||||||
|
* queue.next();
|
||||||
|
* })
|
||||||
|
*/
|
||||||
|
export class ReadWriteQueue {
|
||||||
|
private filePath: string;
|
||||||
|
private queue: ContentAndCallback[];
|
||||||
|
|
||||||
|
constructor (filePath: string) {
|
||||||
|
this.queue = [];
|
||||||
|
this.filePath = filePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
next () {
|
||||||
|
if (this.queue.length === 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const task = this.queue[0];
|
||||||
|
|
||||||
|
if (task[0] == "write") {
|
||||||
|
Fs.writeFile(this.filePath, task[1], "utf-8", (err) => {
|
||||||
|
this.queue.shift();
|
||||||
|
const cb = task[2] as Callback;
|
||||||
|
cb(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else if (task[0] == "read") {
|
||||||
|
Fs.readFile(this.filePath, { encoding: "utf-8"} , (err, data) => {
|
||||||
|
this.queue.shift();
|
||||||
|
const cb = task[1] as Callback;
|
||||||
|
cb(err, data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
write (content: string, cb: Callback) {
|
||||||
|
this.queue.push(["write", content, cb]);
|
||||||
|
if (this.queue.length === 1) {
|
||||||
|
this.next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
read (cb: Callback) {
|
||||||
|
this.queue.push(["read", cb]);
|
||||||
|
if (this.queue.length === 1) {
|
||||||
|
this.next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
|
||||||
|
import BluebirdPromise = require("bluebird");
|
||||||
|
|
||||||
|
export interface ISession {
|
||||||
|
open(): BluebirdPromise<void>;
|
||||||
|
close(): BluebirdPromise<void>;
|
||||||
|
|
||||||
|
searchUserDn(username: string): BluebirdPromise<string>;
|
||||||
|
searchEmails(username: string): BluebirdPromise<string[]>;
|
||||||
|
searchGroups(username: string): BluebirdPromise<string[]>;
|
||||||
|
modifyPassword(username: string, newPassword: string): BluebirdPromise<void>;
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
|
||||||
|
import { ISession } from "./ISession";
|
||||||
|
|
||||||
|
export interface ISessionFactory {
|
||||||
|
create(userDN: string, password: string): ISession;
|
||||||
|
}
|
|
@ -0,0 +1,386 @@
|
||||||
|
import Assert = require("assert");
|
||||||
|
import Bluebird = require("bluebird");
|
||||||
|
|
||||||
|
import { LdapUsersDatabase } from "./LdapUsersDatabase";
|
||||||
|
|
||||||
|
import { SessionFactoryStub } from "./SessionFactoryStub.spec";
|
||||||
|
import { SessionStub } from "./SessionStub.spec";
|
||||||
|
|
||||||
|
const ADMIN_USER_DN = "cn=admin,dc=example,dc=com";
|
||||||
|
const ADMIN_PASSWORD = "password";
|
||||||
|
|
||||||
|
describe("ldap/connector/LdapUsersDatabase", function() {
|
||||||
|
let sessionFactory: SessionFactoryStub;
|
||||||
|
let usersDatabase: LdapUsersDatabase;
|
||||||
|
|
||||||
|
const USERNAME = "user";
|
||||||
|
const PASSWORD = "pass";
|
||||||
|
const NEW_PASSWORD = "pass2";
|
||||||
|
|
||||||
|
const LDAP_CONFIG = {
|
||||||
|
url: "http://localhost:324",
|
||||||
|
additional_users_dn: "ou=users",
|
||||||
|
additional_groups_dn: "ou=groups",
|
||||||
|
base_dn: "dc=example,dc=com",
|
||||||
|
users_filter: "cn={0}",
|
||||||
|
groups_filter: "member={0}",
|
||||||
|
mail_attribute: "mail",
|
||||||
|
group_name_attribute: "cn",
|
||||||
|
user: ADMIN_USER_DN,
|
||||||
|
password: ADMIN_PASSWORD
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(function() {
|
||||||
|
sessionFactory = new SessionFactoryStub();
|
||||||
|
usersDatabase = new LdapUsersDatabase(sessionFactory, LDAP_CONFIG);
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("checkUserPassword", function() {
|
||||||
|
it("should return groups and emails when user/password matches", function() {
|
||||||
|
const USER_DN = `cn=${USERNAME},dc=example,dc=com`;
|
||||||
|
const emails = ["email1", "email2"];
|
||||||
|
const groups = ["group1", "group2"];
|
||||||
|
|
||||||
|
const adminSession = new SessionStub();
|
||||||
|
const userSession = new SessionStub();
|
||||||
|
|
||||||
|
sessionFactory.createStub.withArgs(ADMIN_USER_DN, ADMIN_PASSWORD).returns(adminSession);
|
||||||
|
sessionFactory.createStub.withArgs(USER_DN, PASSWORD).returns(userSession);
|
||||||
|
|
||||||
|
adminSession.openStub.returns(Bluebird.resolve());
|
||||||
|
adminSession.closeStub.returns(Bluebird.resolve());
|
||||||
|
adminSession.searchUserDnStub.returns(Bluebird.resolve(USER_DN));
|
||||||
|
adminSession.searchEmailsStub.withArgs(USERNAME).returns(Bluebird.resolve(emails));
|
||||||
|
adminSession.searchGroupsStub.withArgs(USERNAME).returns(Bluebird.resolve(groups));
|
||||||
|
|
||||||
|
userSession.openStub.returns(Bluebird.resolve());
|
||||||
|
userSession.closeStub.returns(Bluebird.resolve());
|
||||||
|
|
||||||
|
return usersDatabase.checkUserPassword(USERNAME, PASSWORD)
|
||||||
|
.then((groupsAndEmails) => {
|
||||||
|
Assert.deepEqual(groupsAndEmails.groups, groups);
|
||||||
|
Assert.deepEqual(groupsAndEmails.emails, emails);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fail when username/password is wrong", function() {
|
||||||
|
const USER_DN = `cn=${USERNAME},dc=example,dc=com`;
|
||||||
|
|
||||||
|
const adminSession = new SessionStub();
|
||||||
|
const userSession = new SessionStub();
|
||||||
|
|
||||||
|
sessionFactory.createStub.withArgs(ADMIN_USER_DN, ADMIN_PASSWORD).returns(adminSession);
|
||||||
|
sessionFactory.createStub.withArgs(USER_DN, PASSWORD).returns(userSession);
|
||||||
|
|
||||||
|
adminSession.openStub.returns(Bluebird.resolve());
|
||||||
|
adminSession.closeStub.returns(Bluebird.resolve());
|
||||||
|
adminSession.searchUserDnStub.returns(Bluebird.resolve(USER_DN));
|
||||||
|
|
||||||
|
userSession.openStub.returns(Bluebird.reject(new Error("Failed binding")));
|
||||||
|
userSession.closeStub.returns(Bluebird.resolve());
|
||||||
|
|
||||||
|
return usersDatabase.checkUserPassword(USERNAME, PASSWORD)
|
||||||
|
.then(() => Bluebird.reject(new Error("should not be here")))
|
||||||
|
.catch((err) => {
|
||||||
|
Assert(userSession.closeStub.called);
|
||||||
|
Assert(adminSession.closeStub.called);
|
||||||
|
return Bluebird.resolve();
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fail when admin binding fails", function() {
|
||||||
|
const USER_DN = `cn=${USERNAME},dc=example,dc=com`;
|
||||||
|
|
||||||
|
const adminSession = new SessionStub();
|
||||||
|
const userSession = new SessionStub();
|
||||||
|
|
||||||
|
sessionFactory.createStub.withArgs(ADMIN_USER_DN, ADMIN_PASSWORD).returns(adminSession);
|
||||||
|
sessionFactory.createStub.withArgs(USER_DN, PASSWORD).returns(userSession);
|
||||||
|
|
||||||
|
adminSession.openStub.returns(Bluebird.reject(new Error("Failed binding")));
|
||||||
|
adminSession.closeStub.returns(Bluebird.resolve());
|
||||||
|
adminSession.searchUserDnStub.returns(Bluebird.resolve(USER_DN));
|
||||||
|
|
||||||
|
return usersDatabase.checkUserPassword(USERNAME, PASSWORD)
|
||||||
|
.then(() => Bluebird.reject(new Error("should not be here")))
|
||||||
|
.catch((err) => {
|
||||||
|
Assert(userSession.closeStub.notCalled);
|
||||||
|
Assert(adminSession.closeStub.called);
|
||||||
|
return Bluebird.resolve();
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fail when search for user dn fails", function() {
|
||||||
|
const USER_DN = `cn=${USERNAME},dc=example,dc=com`;
|
||||||
|
|
||||||
|
const adminSession = new SessionStub();
|
||||||
|
const userSession = new SessionStub();
|
||||||
|
|
||||||
|
sessionFactory.createStub.withArgs(ADMIN_USER_DN, ADMIN_PASSWORD).returns(adminSession);
|
||||||
|
sessionFactory.createStub.withArgs(USER_DN, PASSWORD).returns(userSession);
|
||||||
|
|
||||||
|
adminSession.openStub.returns(Bluebird.resolve());
|
||||||
|
adminSession.closeStub.returns(Bluebird.resolve());
|
||||||
|
adminSession.searchUserDnStub.returns(Bluebird.reject(new Error("Failed searching user dn")));
|
||||||
|
|
||||||
|
return usersDatabase.checkUserPassword(USERNAME, PASSWORD)
|
||||||
|
.then(() => Bluebird.reject(new Error("should not be here")))
|
||||||
|
.catch((err) => {
|
||||||
|
Assert(userSession.closeStub.notCalled);
|
||||||
|
Assert(adminSession.closeStub.called);
|
||||||
|
return Bluebird.resolve();
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fail when groups retrieval fails", function() {
|
||||||
|
const USER_DN = `cn=${USERNAME},dc=example,dc=com`;
|
||||||
|
const emails = ["email1", "email2"];
|
||||||
|
const groups = ["group1", "group2"];
|
||||||
|
|
||||||
|
const adminSession = new SessionStub();
|
||||||
|
const userSession = new SessionStub();
|
||||||
|
|
||||||
|
sessionFactory.createStub.withArgs(ADMIN_USER_DN, ADMIN_PASSWORD).returns(adminSession);
|
||||||
|
sessionFactory.createStub.withArgs(USER_DN, PASSWORD).returns(userSession);
|
||||||
|
|
||||||
|
adminSession.openStub.returns(Bluebird.resolve());
|
||||||
|
adminSession.closeStub.returns(Bluebird.resolve());
|
||||||
|
adminSession.searchUserDnStub.returns(Bluebird.resolve(USER_DN));
|
||||||
|
adminSession.searchEmailsStub.withArgs(USERNAME)
|
||||||
|
.returns(Bluebird.resolve(emails));
|
||||||
|
adminSession.searchGroupsStub.withArgs(USERNAME)
|
||||||
|
.returns(Bluebird.reject(new Error("Failed retrieving groups")));
|
||||||
|
|
||||||
|
userSession.openStub.returns(Bluebird.resolve());
|
||||||
|
userSession.closeStub.returns(Bluebird.resolve());
|
||||||
|
|
||||||
|
return usersDatabase.checkUserPassword(USERNAME, PASSWORD)
|
||||||
|
.then((groupsAndEmails) => Bluebird.reject(new Error("should not be here")))
|
||||||
|
.catch((err) => {
|
||||||
|
Assert(userSession.closeStub.called);
|
||||||
|
Assert(adminSession.closeStub.called);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fail when emails retrieval fails", function() {
|
||||||
|
const USER_DN = `cn=${USERNAME},dc=example,dc=com`;
|
||||||
|
const emails = ["email1", "email2"];
|
||||||
|
const groups = ["group1", "group2"];
|
||||||
|
|
||||||
|
const adminSession = new SessionStub();
|
||||||
|
const userSession = new SessionStub();
|
||||||
|
|
||||||
|
sessionFactory.createStub.withArgs(ADMIN_USER_DN, ADMIN_PASSWORD).returns(adminSession);
|
||||||
|
sessionFactory.createStub.withArgs(USER_DN, PASSWORD).returns(userSession);
|
||||||
|
|
||||||
|
adminSession.openStub.returns(Bluebird.resolve());
|
||||||
|
adminSession.closeStub.returns(Bluebird.resolve());
|
||||||
|
adminSession.searchUserDnStub.returns(Bluebird.resolve(USER_DN));
|
||||||
|
adminSession.searchEmailsStub.withArgs(USERNAME)
|
||||||
|
.returns(Bluebird.reject(new Error("Emails retrieval failed")));
|
||||||
|
adminSession.searchGroupsStub.withArgs(USERNAME)
|
||||||
|
.returns(Bluebird.resolve(groups));
|
||||||
|
|
||||||
|
userSession.openStub.returns(Bluebird.resolve());
|
||||||
|
userSession.closeStub.returns(Bluebird.resolve());
|
||||||
|
|
||||||
|
return usersDatabase.checkUserPassword(USERNAME, PASSWORD)
|
||||||
|
.then((groupsAndEmails) => Bluebird.reject(new Error("should not be here")))
|
||||||
|
.catch((err) => {
|
||||||
|
Assert(userSession.closeStub.called);
|
||||||
|
Assert(adminSession.closeStub.called);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getEmails", function() {
|
||||||
|
it("should succefully retrieves email", () => {
|
||||||
|
const emails = ["email1", "email2"];
|
||||||
|
const session = new SessionStub();
|
||||||
|
sessionFactory.createStub.withArgs(ADMIN_USER_DN, ADMIN_PASSWORD).returns(session);
|
||||||
|
|
||||||
|
session.openStub.returns(Bluebird.resolve());
|
||||||
|
session.closeStub.returns(Bluebird.resolve());
|
||||||
|
session.searchEmailsStub.returns(Bluebird.resolve(emails));
|
||||||
|
|
||||||
|
return usersDatabase.getEmails(USERNAME)
|
||||||
|
.then((foundEmails) => {
|
||||||
|
Assert(session.closeStub.called);
|
||||||
|
Assert.deepEqual(foundEmails, emails);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fail when binding fails", () => {
|
||||||
|
const emails = ["email1", "email2"];
|
||||||
|
const session = new SessionStub();
|
||||||
|
sessionFactory.createStub.withArgs(ADMIN_USER_DN, ADMIN_PASSWORD).returns(session);
|
||||||
|
|
||||||
|
session.openStub.returns(Bluebird.reject(new Error("Binding failed")));
|
||||||
|
|
||||||
|
return usersDatabase.getEmails(USERNAME)
|
||||||
|
.then(() => Bluebird.reject(new Error("should not be here")))
|
||||||
|
.catch((err) => {
|
||||||
|
Assert(session.closeStub.called);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fail when unbinding fails", () => {
|
||||||
|
const emails = ["email1", "email2"];
|
||||||
|
const session = new SessionStub();
|
||||||
|
sessionFactory.createStub.withArgs(ADMIN_USER_DN, ADMIN_PASSWORD).returns(session);
|
||||||
|
|
||||||
|
session.openStub.returns(Bluebird.resolve());
|
||||||
|
session.searchEmailsStub.returns(Bluebird.resolve(emails));
|
||||||
|
session.closeStub.returns(Bluebird.reject(new Error("Unbinding failed")));
|
||||||
|
|
||||||
|
return usersDatabase.getEmails(USERNAME)
|
||||||
|
.then(() => Bluebird.reject(new Error("should not be here")))
|
||||||
|
.catch((err) => {
|
||||||
|
Assert(session.closeStub.called);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fail when search fails", () => {
|
||||||
|
const emails = ["email1", "email2"];
|
||||||
|
const session = new SessionStub();
|
||||||
|
sessionFactory.createStub.withArgs(ADMIN_USER_DN, ADMIN_PASSWORD).returns(session);
|
||||||
|
|
||||||
|
session.openStub.returns(Bluebird.resolve());
|
||||||
|
session.searchEmailsStub.returns(Bluebird.reject(new Error("Search failed")));
|
||||||
|
session.closeStub.returns(Bluebird.resolve());
|
||||||
|
|
||||||
|
return usersDatabase.getEmails(USERNAME)
|
||||||
|
.then(() => Bluebird.reject(new Error("should not be here")))
|
||||||
|
.catch((err) => {
|
||||||
|
Assert(session.closeStub.called);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
describe("getGroups", function() {
|
||||||
|
it("should succefully retrieves groups", () => {
|
||||||
|
const groups = ["group1", "group2"];
|
||||||
|
const session = new SessionStub();
|
||||||
|
sessionFactory.createStub.withArgs(ADMIN_USER_DN, ADMIN_PASSWORD).returns(session);
|
||||||
|
|
||||||
|
session.openStub.returns(Bluebird.resolve());
|
||||||
|
session.closeStub.returns(Bluebird.resolve());
|
||||||
|
session.searchGroupsStub.returns(Bluebird.resolve(groups));
|
||||||
|
|
||||||
|
return usersDatabase.getGroups(USERNAME)
|
||||||
|
.then((foundGroups) => {
|
||||||
|
Assert(session.closeStub.called);
|
||||||
|
Assert.deepEqual(foundGroups, groups);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fail when binding fails", () => {
|
||||||
|
const session = new SessionStub();
|
||||||
|
sessionFactory.createStub.withArgs(ADMIN_USER_DN, ADMIN_PASSWORD).returns(session);
|
||||||
|
|
||||||
|
session.openStub.returns(Bluebird.reject(new Error("Binding failed")));
|
||||||
|
|
||||||
|
return usersDatabase.getGroups(USERNAME)
|
||||||
|
.then(() => Bluebird.reject(new Error("should not be here")))
|
||||||
|
.catch((err) => {
|
||||||
|
Assert(session.closeStub.called);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fail when unbinding fails", () => {
|
||||||
|
const groups = ["group1", "group2"];
|
||||||
|
const session = new SessionStub();
|
||||||
|
sessionFactory.createStub.withArgs(ADMIN_USER_DN, ADMIN_PASSWORD).returns(session);
|
||||||
|
|
||||||
|
session.openStub.returns(Bluebird.resolve());
|
||||||
|
session.searchGroupsStub.returns(Bluebird.resolve(groups));
|
||||||
|
session.closeStub.returns(Bluebird.reject(new Error("Unbinding failed")));
|
||||||
|
|
||||||
|
return usersDatabase.getGroups(USERNAME)
|
||||||
|
.then(() => Bluebird.reject(new Error("should not be here")))
|
||||||
|
.catch((err) => {
|
||||||
|
Assert(session.closeStub.called);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fail when search fails", () => {
|
||||||
|
const groups = ["group1", "group2"];
|
||||||
|
const session = new SessionStub();
|
||||||
|
sessionFactory.createStub.withArgs(ADMIN_USER_DN, ADMIN_PASSWORD).returns(session);
|
||||||
|
|
||||||
|
session.openStub.returns(Bluebird.resolve());
|
||||||
|
session.searchGroupsStub.returns(Bluebird.reject(new Error("Search failed")));
|
||||||
|
session.closeStub.returns(Bluebird.resolve());
|
||||||
|
|
||||||
|
return usersDatabase.getGroups(USERNAME)
|
||||||
|
.then(() => Bluebird.reject(new Error("should not be here")))
|
||||||
|
.catch((err) => {
|
||||||
|
Assert(session.closeStub.called);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
describe("updatePassword", function() {
|
||||||
|
it("should successfully update password", () => {
|
||||||
|
const session = new SessionStub();
|
||||||
|
sessionFactory.createStub.withArgs(ADMIN_USER_DN, ADMIN_PASSWORD).returns(session);
|
||||||
|
|
||||||
|
session.openStub.returns(Bluebird.resolve());
|
||||||
|
session.closeStub.returns(Bluebird.resolve());
|
||||||
|
session.modifyPasswordStub.returns(Bluebird.resolve());
|
||||||
|
|
||||||
|
return usersDatabase.updatePassword(USERNAME, NEW_PASSWORD)
|
||||||
|
.then(() => {
|
||||||
|
Assert(session.modifyPasswordStub.calledWith(USERNAME, NEW_PASSWORD));
|
||||||
|
Assert(session.closeStub.called);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fail when binding fails", () => {
|
||||||
|
const session = new SessionStub();
|
||||||
|
sessionFactory.createStub.withArgs(ADMIN_USER_DN, ADMIN_PASSWORD).returns(session);
|
||||||
|
|
||||||
|
session.openStub.returns(Bluebird.reject(new Error("Binding failed")));
|
||||||
|
session.closeStub.returns(Bluebird.resolve());
|
||||||
|
session.modifyPasswordStub.returns(Bluebird.resolve());
|
||||||
|
|
||||||
|
return usersDatabase.updatePassword(USERNAME, NEW_PASSWORD)
|
||||||
|
.then(() => Bluebird.reject(new Error("should not be here")))
|
||||||
|
.catch(() => {
|
||||||
|
Assert(session.closeStub.called);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fail when update fails", () => {
|
||||||
|
const session = new SessionStub();
|
||||||
|
sessionFactory.createStub.withArgs(ADMIN_USER_DN, ADMIN_PASSWORD).returns(session);
|
||||||
|
|
||||||
|
session.openStub.returns(Bluebird.resolve());
|
||||||
|
session.closeStub.returns(Bluebird.reject(new Error("Update failed")));
|
||||||
|
session.modifyPasswordStub.returns(Bluebird.resolve());
|
||||||
|
|
||||||
|
return usersDatabase.updatePassword(USERNAME, NEW_PASSWORD)
|
||||||
|
.then(() => Bluebird.reject(new Error("should not be here")))
|
||||||
|
.catch(() => {
|
||||||
|
Assert(session.closeStub.called);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fail when unbind fails", () => {
|
||||||
|
const session = new SessionStub();
|
||||||
|
sessionFactory.createStub.withArgs(ADMIN_USER_DN, ADMIN_PASSWORD).returns(session);
|
||||||
|
|
||||||
|
session.openStub.returns(Bluebird.resolve());
|
||||||
|
session.closeStub.returns(Bluebird.resolve());
|
||||||
|
session.modifyPasswordStub.returns(Bluebird.reject(new Error("Unbind failed")));
|
||||||
|
|
||||||
|
return usersDatabase.updatePassword(USERNAME, NEW_PASSWORD)
|
||||||
|
.then(() => Bluebird.reject(new Error("should not be here")))
|
||||||
|
.catch(() => {
|
||||||
|
Assert(session.closeStub.called);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,107 @@
|
||||||
|
import Bluebird = require("bluebird");
|
||||||
|
import { IUsersDatabase } from "../IUsersDatabase";
|
||||||
|
import { ISessionFactory } from "./ISessionFactory";
|
||||||
|
import { LdapConfiguration } from "../../../configuration/schema/LdapConfiguration";
|
||||||
|
import { ISession } from "./ISession";
|
||||||
|
import { GroupsAndEmails } from "../GroupsAndEmails";
|
||||||
|
import Exceptions = require("../../../Exceptions");
|
||||||
|
|
||||||
|
type SessionCallback<T> = (session: ISession) => Bluebird<T>;
|
||||||
|
|
||||||
|
export class LdapUsersDatabase implements IUsersDatabase {
|
||||||
|
private sessionFactory: ISessionFactory;
|
||||||
|
private configuration: LdapConfiguration;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
sessionFactory: ISessionFactory,
|
||||||
|
configuration: LdapConfiguration) {
|
||||||
|
this.sessionFactory = sessionFactory;
|
||||||
|
this.configuration = configuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
private withSession<T>(
|
||||||
|
username: string,
|
||||||
|
password: string,
|
||||||
|
cb: SessionCallback<T>): Bluebird<T> {
|
||||||
|
const session = this.sessionFactory.create(username, password);
|
||||||
|
return session.open()
|
||||||
|
.then(() => cb(session))
|
||||||
|
.finally(() => session.close());
|
||||||
|
}
|
||||||
|
|
||||||
|
checkUserPassword(username: string, password: string): Bluebird<GroupsAndEmails> {
|
||||||
|
const that = this;
|
||||||
|
function verifyUserPassword(userDN: string) {
|
||||||
|
return that.withSession<void>(
|
||||||
|
userDN,
|
||||||
|
password,
|
||||||
|
(session) => Bluebird.resolve()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getInfo(session: ISession) {
|
||||||
|
return Bluebird.join(
|
||||||
|
session.searchGroups(username),
|
||||||
|
session.searchEmails(username)
|
||||||
|
)
|
||||||
|
.spread((groups: string[], emails: string[]) => {
|
||||||
|
return { groups: groups, emails: emails };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return that.withSession(
|
||||||
|
that.configuration.user,
|
||||||
|
that.configuration.password,
|
||||||
|
(session) => {
|
||||||
|
return session.searchUserDn(username)
|
||||||
|
.then(verifyUserPassword)
|
||||||
|
.then(() => getInfo(session));
|
||||||
|
})
|
||||||
|
.catch((err) =>
|
||||||
|
Bluebird.reject(new Exceptions.LdapError(err.message)));
|
||||||
|
}
|
||||||
|
|
||||||
|
getEmails(username: string): Bluebird<string[]> {
|
||||||
|
const that = this;
|
||||||
|
return that.withSession(
|
||||||
|
that.configuration.user,
|
||||||
|
that.configuration.password,
|
||||||
|
(session) => {
|
||||||
|
return session.searchEmails(username);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.catch((err) =>
|
||||||
|
Bluebird.reject(new Exceptions.LdapError("Failed during email retrieval: " + err.message))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getGroups(username: string): Bluebird<string[]> {
|
||||||
|
const that = this;
|
||||||
|
return that.withSession(
|
||||||
|
that.configuration.user,
|
||||||
|
that.configuration.password,
|
||||||
|
(session) => {
|
||||||
|
return session.searchGroups(username);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.catch((err) =>
|
||||||
|
Bluebird.reject(new Exceptions.LdapError("Failed during email retrieval: " + err.message))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePassword(username: string, newPassword: string): Bluebird<void> {
|
||||||
|
const that = this;
|
||||||
|
return that.withSession(
|
||||||
|
that.configuration.user,
|
||||||
|
that.configuration.password,
|
||||||
|
(session) => {
|
||||||
|
return session.modifyPassword(username, newPassword);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.catch(function (err: Error) {
|
||||||
|
return Bluebird.reject(
|
||||||
|
new Exceptions.LdapError(
|
||||||
|
"Error while updating password: " + err.message));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
import BluebirdPromise = require("bluebird");
|
||||||
|
import { SessionStub } from "./SessionStub.spec";
|
||||||
|
import { SafeSession } from "./SafeSession";
|
||||||
|
|
||||||
|
describe("ldap/SanitizedClient", function () {
|
||||||
|
let client: SafeSession;
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
const clientStub = new SessionStub();
|
||||||
|
clientStub.searchUserDnStub.onCall(0).returns(BluebirdPromise.resolve());
|
||||||
|
clientStub.searchGroupsStub.onCall(0).returns(BluebirdPromise.resolve());
|
||||||
|
clientStub.searchEmailsStub.onCall(0).returns(BluebirdPromise.resolve());
|
||||||
|
clientStub.modifyPasswordStub.onCall(0).returns(BluebirdPromise.resolve());
|
||||||
|
client = new SafeSession(clientStub);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("special chars are used", function () {
|
||||||
|
it("should fail when special chars are used in searchUserDn", function () {
|
||||||
|
// potential ldap injection";
|
||||||
|
return client.searchUserDn("cn=dummy_user,ou=groupgs")
|
||||||
|
.then(function () {
|
||||||
|
return BluebirdPromise.reject(new Error("Should not be here."));
|
||||||
|
}, function () {
|
||||||
|
return BluebirdPromise.resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fail when special chars are used in searchGroups", function () {
|
||||||
|
// potential ldap injection";
|
||||||
|
return client.searchGroups("cn=dummy_user,ou=groupgs")
|
||||||
|
.then(function () {
|
||||||
|
return BluebirdPromise.reject(new Error("Should not be here."));
|
||||||
|
}, function () {
|
||||||
|
return BluebirdPromise.resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fail when special chars are used in searchEmails", function () {
|
||||||
|
// potential ldap injection";
|
||||||
|
return client.searchEmails("cn=dummy_user,ou=groupgs")
|
||||||
|
.then(function () {
|
||||||
|
return BluebirdPromise.reject(new Error("Should not be here."));
|
||||||
|
}, function () {
|
||||||
|
return BluebirdPromise.resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fail when special chars are used in modifyPassword", function () {
|
||||||
|
// potential ldap injection";
|
||||||
|
return client.modifyPassword("cn=dummy_user,ou=groupgs", "abc")
|
||||||
|
.then(function () {
|
||||||
|
return BluebirdPromise.reject(new Error("Should not be here."));
|
||||||
|
}, function () {
|
||||||
|
return BluebirdPromise.resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("no special chars are used", function() {
|
||||||
|
it("should succeed when no special chars are used in searchUserDn", function () {
|
||||||
|
return client.searchUserDn("dummy_user");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should succeed when no special chars are used in searchGroups", function () {
|
||||||
|
return client.searchGroups("dummy_user");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should succeed when no special chars are used in searchEmails", function () {
|
||||||
|
return client.searchEmails("dummy_user");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should succeed when no special chars are used in modifyPassword", function () {
|
||||||
|
return client.modifyPassword("dummy_user", "abc");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,62 @@
|
||||||
|
import BluebirdPromise = require("bluebird");
|
||||||
|
import { ISession } from "./ISession";
|
||||||
|
import { Sanitizer } from "./Sanitizer";
|
||||||
|
|
||||||
|
const SPECIAL_CHAR_USED_MESSAGE = "Special character used in LDAP query.";
|
||||||
|
|
||||||
|
|
||||||
|
export class SafeSession implements ISession {
|
||||||
|
private sesion: ISession;
|
||||||
|
|
||||||
|
constructor(sesion: ISession) {
|
||||||
|
this.sesion = sesion;
|
||||||
|
}
|
||||||
|
|
||||||
|
open(): BluebirdPromise<void> {
|
||||||
|
return this.sesion.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
close(): BluebirdPromise<void> {
|
||||||
|
return this.sesion.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
searchGroups(username: string): BluebirdPromise<string[]> {
|
||||||
|
try {
|
||||||
|
const sanitizedUsername = Sanitizer.sanitize(username);
|
||||||
|
return this.sesion.searchGroups(sanitizedUsername);
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
return BluebirdPromise.reject(new Error(SPECIAL_CHAR_USED_MESSAGE));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
searchUserDn(username: string): BluebirdPromise<string> {
|
||||||
|
try {
|
||||||
|
const sanitizedUsername = Sanitizer.sanitize(username);
|
||||||
|
return this.sesion.searchUserDn(sanitizedUsername);
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
return BluebirdPromise.reject(new Error(SPECIAL_CHAR_USED_MESSAGE));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
searchEmails(username: string): BluebirdPromise<string[]> {
|
||||||
|
try {
|
||||||
|
const sanitizedUsername = Sanitizer.sanitize(username);
|
||||||
|
return this.sesion.searchEmails(sanitizedUsername);
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
return BluebirdPromise.reject(new Error(SPECIAL_CHAR_USED_MESSAGE));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
modifyPassword(username: string, newPassword: string): BluebirdPromise<void> {
|
||||||
|
try {
|
||||||
|
const sanitizedUsername = Sanitizer.sanitize(username);
|
||||||
|
return this.sesion.modifyPassword(sanitizedUsername, newPassword);
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
return BluebirdPromise.reject(new Error(SPECIAL_CHAR_USED_MESSAGE));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
import Assert = require("assert");
|
||||||
|
import { Sanitizer } from "./Sanitizer";
|
||||||
|
|
||||||
|
describe("ldap/InputsSanitizer", function () {
|
||||||
|
it("should fail when special characters are used", function () {
|
||||||
|
Assert.throws(() => { Sanitizer.sanitize("ab,c"); }, Error);
|
||||||
|
Assert.throws(() => { Sanitizer.sanitize("a\\bc"); }, Error);
|
||||||
|
Assert.throws(() => { Sanitizer.sanitize("a'bc"); }, Error);
|
||||||
|
Assert.throws(() => { Sanitizer.sanitize("a#bc"); }, Error);
|
||||||
|
Assert.throws(() => { Sanitizer.sanitize("a+bc"); }, Error);
|
||||||
|
Assert.throws(() => { Sanitizer.sanitize("a<bc"); }, Error);
|
||||||
|
Assert.throws(() => { Sanitizer.sanitize("a>bc"); }, Error);
|
||||||
|
Assert.throws(() => { Sanitizer.sanitize("a;bc"); }, Error);
|
||||||
|
Assert.throws(() => { Sanitizer.sanitize("a\"bc"); }, Error);
|
||||||
|
Assert.throws(() => { Sanitizer.sanitize("a=bc"); }, Error);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return original string", function () {
|
||||||
|
Assert.equal(Sanitizer.sanitize("abcdef"), "abcdef");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should trim", function () {
|
||||||
|
Assert.throws(() => { Sanitizer.sanitize(" abc "); }, Error);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,25 @@
|
||||||
|
|
||||||
|
// returns true for 1 or more matches, where 'a' is an array and 'b' is a search string or an array of multiple search strings
|
||||||
|
function contains(a: string, character: string) {
|
||||||
|
// string match
|
||||||
|
return a.indexOf(character) > -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function containsOneOf(s: string, characters: string[]) {
|
||||||
|
return characters
|
||||||
|
.map((character: string) => { return contains(s, character); })
|
||||||
|
.reduce((acc: boolean, current: boolean) => { return acc || current; }, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Sanitizer {
|
||||||
|
static sanitize(input: string): string {
|
||||||
|
const forbiddenChars = [",", "\\", "'", "#", "+", "<", ">", ";", "\"", "="];
|
||||||
|
if (containsOneOf(input, forbiddenChars))
|
||||||
|
throw new Error("Input containing unsafe characters.");
|
||||||
|
|
||||||
|
if (input != input.trim())
|
||||||
|
throw new Error("Input has unexpected spaces.");
|
||||||
|
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,127 @@
|
||||||
|
|
||||||
|
import { LdapConfiguration } from "../../../configuration/schema/LdapConfiguration";
|
||||||
|
import { Session } from "./Session";
|
||||||
|
import { ConnectorFactoryStub } from "./connector/ConnectorFactoryStub.spec";
|
||||||
|
import { ConnectorStub } from "./connector/ConnectorStub.spec";
|
||||||
|
|
||||||
|
import Sinon = require("sinon");
|
||||||
|
import BluebirdPromise = require("bluebird");
|
||||||
|
import Assert = require("assert");
|
||||||
|
import Winston = require("winston");
|
||||||
|
|
||||||
|
describe("ldap/Session", function () {
|
||||||
|
const USERNAME = "username";
|
||||||
|
const ADMIN_USER_DN = "cn=admin,dc=example,dc=com";
|
||||||
|
const ADMIN_PASSWORD = "password";
|
||||||
|
|
||||||
|
it("should replace {0} by username when searching for groups in LDAP", function () {
|
||||||
|
const options: LdapConfiguration = {
|
||||||
|
url: "ldap://ldap",
|
||||||
|
additional_users_dn: "ou=users",
|
||||||
|
additional_groups_dn: "ou=groups",
|
||||||
|
base_dn: "dc=example,dc=com",
|
||||||
|
users_filter: "cn={0}",
|
||||||
|
groups_filter: "member=cn={0},ou=users,dc=example,dc=com",
|
||||||
|
group_name_attribute: "cn",
|
||||||
|
mail_attribute: "mail",
|
||||||
|
user: "cn=admin,dc=example,dc=com",
|
||||||
|
password: "password"
|
||||||
|
};
|
||||||
|
const connectorStub = new ConnectorStub();
|
||||||
|
connectorStub.searchAsyncStub.returns(BluebirdPromise.resolve([{
|
||||||
|
cn: "group1"
|
||||||
|
}]));
|
||||||
|
const client = new Session(ADMIN_USER_DN, ADMIN_PASSWORD, options, connectorStub, Winston);
|
||||||
|
|
||||||
|
return client.searchGroups("user1")
|
||||||
|
.then(function () {
|
||||||
|
Assert.equal(connectorStub.searchAsyncStub.getCall(0).args[1].filter,
|
||||||
|
"member=cn=user1,ou=users,dc=example,dc=com");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should replace {dn} by user DN when searching for groups in LDAP", function () {
|
||||||
|
const USER_DN = "cn=user1,ou=users,dc=example,dc=com";
|
||||||
|
const options: LdapConfiguration = {
|
||||||
|
url: "ldap://ldap",
|
||||||
|
additional_users_dn: "ou=users",
|
||||||
|
additional_groups_dn: "ou=groups",
|
||||||
|
base_dn: "dc=example,dc=com",
|
||||||
|
users_filter: "cn={0}",
|
||||||
|
groups_filter: "member={dn}",
|
||||||
|
group_name_attribute: "cn",
|
||||||
|
mail_attribute: "mail",
|
||||||
|
user: "cn=admin,dc=example,dc=com",
|
||||||
|
password: "password"
|
||||||
|
};
|
||||||
|
const ldapClient = new ConnectorStub();
|
||||||
|
|
||||||
|
// Retrieve user DN
|
||||||
|
ldapClient.searchAsyncStub.withArgs("ou=users,dc=example,dc=com", {
|
||||||
|
scope: "sub",
|
||||||
|
sizeLimit: 1,
|
||||||
|
attributes: ["dn"],
|
||||||
|
filter: "cn=user1"
|
||||||
|
}).returns(BluebirdPromise.resolve([{
|
||||||
|
dn: USER_DN
|
||||||
|
}]));
|
||||||
|
|
||||||
|
// Retrieve groups
|
||||||
|
ldapClient.searchAsyncStub.withArgs("ou=groups,dc=example,dc=com", {
|
||||||
|
scope: "sub",
|
||||||
|
attributes: ["cn"],
|
||||||
|
filter: "member=" + USER_DN
|
||||||
|
}).returns(BluebirdPromise.resolve([{
|
||||||
|
cn: "group1"
|
||||||
|
}]));
|
||||||
|
|
||||||
|
const client = new Session(ADMIN_USER_DN, ADMIN_PASSWORD, options, ldapClient, Winston);
|
||||||
|
|
||||||
|
return client.searchGroups("user1")
|
||||||
|
.then(function (groups: string[]) {
|
||||||
|
Assert.deepEqual(groups, ["group1"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should retrieve mail from custom attribute", function () {
|
||||||
|
const USER_DN = "cn=user1,ou=users,dc=example,dc=com";
|
||||||
|
const options: LdapConfiguration = {
|
||||||
|
url: "ldap://ldap",
|
||||||
|
additional_users_dn: "ou=users",
|
||||||
|
additional_groups_dn: "ou=groups",
|
||||||
|
base_dn: "dc=example,dc=com",
|
||||||
|
users_filter: "cn={0}",
|
||||||
|
groups_filter: "member={dn}",
|
||||||
|
group_name_attribute: "cn",
|
||||||
|
mail_attribute: "custom_mail",
|
||||||
|
user: "cn=admin,dc=example,dc=com",
|
||||||
|
password: "password"
|
||||||
|
};
|
||||||
|
const connector = new ConnectorStub();
|
||||||
|
// Retrieve user DN
|
||||||
|
connector.searchAsyncStub.withArgs("ou=users,dc=example,dc=com", {
|
||||||
|
scope: "sub",
|
||||||
|
sizeLimit: 1,
|
||||||
|
attributes: ["dn"],
|
||||||
|
filter: "cn=user1"
|
||||||
|
}).returns(BluebirdPromise.resolve([{
|
||||||
|
dn: USER_DN
|
||||||
|
}]));
|
||||||
|
|
||||||
|
// Retrieve email
|
||||||
|
connector.searchAsyncStub.withArgs("cn=user1,ou=users,dc=example,dc=com", {
|
||||||
|
scope: "base",
|
||||||
|
sizeLimit: 1,
|
||||||
|
attributes: ["custom_mail"],
|
||||||
|
}).returns(BluebirdPromise.resolve([{
|
||||||
|
custom_mail: "user1@example.com"
|
||||||
|
}]));
|
||||||
|
|
||||||
|
const client = new Session(ADMIN_USER_DN, ADMIN_PASSWORD, options, connector, Winston);
|
||||||
|
|
||||||
|
return client.searchEmails("user1")
|
||||||
|
.then(function (emails: string[]) {
|
||||||
|
Assert.deepEqual(emails, ["user1@example.com"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,156 @@
|
||||||
|
import BluebirdPromise = require("bluebird");
|
||||||
|
import exceptions = require("../../../Exceptions");
|
||||||
|
import { EventEmitter } from "events";
|
||||||
|
import { ISession } from "./ISession";
|
||||||
|
import { LdapConfiguration } from "../../../configuration/schema/LdapConfiguration";
|
||||||
|
import { Winston } from "../../../../../types/Dependencies";
|
||||||
|
import Util = require("util");
|
||||||
|
import { HashGenerator } from "../../../utils/HashGenerator";
|
||||||
|
import { IConnector } from "./connector/IConnector";
|
||||||
|
|
||||||
|
export class Session implements ISession {
|
||||||
|
private userDN: string;
|
||||||
|
private password: string;
|
||||||
|
private connector: IConnector;
|
||||||
|
private logger: Winston;
|
||||||
|
private options: LdapConfiguration;
|
||||||
|
|
||||||
|
private groupsSearchBase: string;
|
||||||
|
private usersSearchBase: string;
|
||||||
|
|
||||||
|
constructor(userDN: string, password: string, options: LdapConfiguration,
|
||||||
|
connector: IConnector, logger: Winston) {
|
||||||
|
this.options = options;
|
||||||
|
this.logger = logger;
|
||||||
|
this.userDN = userDN;
|
||||||
|
this.password = password;
|
||||||
|
this.connector = connector;
|
||||||
|
|
||||||
|
this.groupsSearchBase = (this.options.additional_groups_dn)
|
||||||
|
? Util.format("%s,%s", this.options.additional_groups_dn, this.options.base_dn)
|
||||||
|
: this.options.base_dn;
|
||||||
|
|
||||||
|
this.usersSearchBase = (this.options.additional_users_dn)
|
||||||
|
? Util.format("%s,%s", this.options.additional_users_dn, this.options.base_dn)
|
||||||
|
: this.options.base_dn;
|
||||||
|
}
|
||||||
|
|
||||||
|
open(): BluebirdPromise<void> {
|
||||||
|
this.logger.debug("LDAP: Bind user '%s'", this.userDN);
|
||||||
|
return this.connector.bindAsync(this.userDN, this.password)
|
||||||
|
.error(function (err: Error) {
|
||||||
|
return BluebirdPromise.reject(new exceptions.LdapBindError(err.message));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
close(): BluebirdPromise<void> {
|
||||||
|
this.logger.debug("LDAP: Unbind user '%s'", this.userDN);
|
||||||
|
return this.connector.unbindAsync()
|
||||||
|
.error(function (err: Error) {
|
||||||
|
return BluebirdPromise.reject(new exceptions.LdapBindError(err.message));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private createGroupsFilter(userGroupsFilter: string, username: string): BluebirdPromise<string> {
|
||||||
|
if (userGroupsFilter.indexOf("{0}") > 0) {
|
||||||
|
return BluebirdPromise.resolve(userGroupsFilter.replace("{0}", username));
|
||||||
|
}
|
||||||
|
else if (userGroupsFilter.indexOf("{dn}") > 0) {
|
||||||
|
return this.searchUserDn(username)
|
||||||
|
.then(function (userDN: string) {
|
||||||
|
return BluebirdPromise.resolve(userGroupsFilter.replace("{dn}", userDN));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return BluebirdPromise.resolve(userGroupsFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
searchGroups(username: string): BluebirdPromise<string[]> {
|
||||||
|
const that = this;
|
||||||
|
return this.createGroupsFilter(this.options.groups_filter, username)
|
||||||
|
.then(function (groupsFilter: string) {
|
||||||
|
that.logger.debug("Computed groups filter is %s", groupsFilter);
|
||||||
|
const query = {
|
||||||
|
scope: "sub",
|
||||||
|
attributes: [that.options.group_name_attribute],
|
||||||
|
filter: groupsFilter
|
||||||
|
};
|
||||||
|
return that.connector.searchAsync(that.groupsSearchBase, query);
|
||||||
|
})
|
||||||
|
.then(function (docs: { cn: string }[]) {
|
||||||
|
const groups = docs.map((doc: any) => { return doc.cn; });
|
||||||
|
that.logger.debug("LDAP: groups of user %s are [%s]", username, groups.join(","));
|
||||||
|
return BluebirdPromise.resolve(groups);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
searchUserDn(username: string): BluebirdPromise<string> {
|
||||||
|
const that = this;
|
||||||
|
const filter = this.options.users_filter.replace("{0}", username);
|
||||||
|
this.logger.debug("Computed users filter is %s", filter);
|
||||||
|
const query = {
|
||||||
|
scope: "sub",
|
||||||
|
sizeLimit: 1,
|
||||||
|
attributes: ["dn"],
|
||||||
|
filter: filter
|
||||||
|
};
|
||||||
|
|
||||||
|
that.logger.debug("LDAP: searching for user dn of %s", username);
|
||||||
|
return that.connector.searchAsync(this.usersSearchBase, query)
|
||||||
|
.then(function (users: { dn: string }[]) {
|
||||||
|
if (users.length > 0) {
|
||||||
|
that.logger.debug("LDAP: retrieved user dn is %s", users[0].dn);
|
||||||
|
return BluebirdPromise.resolve(users[0].dn);
|
||||||
|
}
|
||||||
|
return BluebirdPromise.reject(new Error(
|
||||||
|
Util.format("No user DN found for user '%s'", username)));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
searchEmails(username: string): BluebirdPromise<string[]> {
|
||||||
|
const that = this;
|
||||||
|
const query = {
|
||||||
|
scope: "base",
|
||||||
|
sizeLimit: 1,
|
||||||
|
attributes: [this.options.mail_attribute]
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.searchUserDn(username)
|
||||||
|
.then(function (userDN) {
|
||||||
|
return that.connector.searchAsync(userDN, query);
|
||||||
|
})
|
||||||
|
.then(function (docs: { [mail_attribute: string]: string }[]) {
|
||||||
|
const emails: string[] = docs
|
||||||
|
.filter((d) => { return typeof d[that.options.mail_attribute] === "string"; })
|
||||||
|
.map((d) => { return d[that.options.mail_attribute]; });
|
||||||
|
that.logger.debug("LDAP: emails of user '%s' are %s", username, emails);
|
||||||
|
return BluebirdPromise.resolve(emails);
|
||||||
|
})
|
||||||
|
.catch(function (err: Error) {
|
||||||
|
return BluebirdPromise.reject(new exceptions.LdapError("Error while searching emails. " + err.stack));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
modifyPassword(username: string, newPassword: string): BluebirdPromise<void> {
|
||||||
|
const that = this;
|
||||||
|
this.logger.debug("LDAP: update password of user '%s'", username);
|
||||||
|
return this.searchUserDn(username)
|
||||||
|
.then(function (userDN: string) {
|
||||||
|
return BluebirdPromise.join(
|
||||||
|
HashGenerator.ssha512(newPassword),
|
||||||
|
BluebirdPromise.resolve(userDN));
|
||||||
|
})
|
||||||
|
.then(function (res: string[]) {
|
||||||
|
const change = {
|
||||||
|
operation: "replace",
|
||||||
|
modification: {
|
||||||
|
userPassword: res[0]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
that.logger.debug("Password new='%s'", change.modification.userPassword);
|
||||||
|
return that.connector.modifyAsync(res[1], change);
|
||||||
|
})
|
||||||
|
.then(function () {
|
||||||
|
return that.connector.unbindAsync();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
import Ldapjs = require("ldapjs");
|
||||||
|
import Winston = require("winston");
|
||||||
|
|
||||||
|
import { IConnectorFactory } from "./connector/IConnectorFactory";
|
||||||
|
import { ISessionFactory } from "./ISessionFactory";
|
||||||
|
import { ISession } from "./ISession";
|
||||||
|
import { LdapConfiguration } from "../../../configuration/schema/LdapConfiguration";
|
||||||
|
import { Session } from "./Session";
|
||||||
|
import { SafeSession } from "./SafeSession";
|
||||||
|
|
||||||
|
|
||||||
|
export class SessionFactory implements ISessionFactory {
|
||||||
|
private config: LdapConfiguration;
|
||||||
|
private connectorFactory: IConnectorFactory;
|
||||||
|
private logger: typeof Winston;
|
||||||
|
|
||||||
|
constructor(ldapConfiguration: LdapConfiguration,
|
||||||
|
connectorFactory: IConnectorFactory,
|
||||||
|
logger: typeof Winston) {
|
||||||
|
this.config = ldapConfiguration;
|
||||||
|
this.connectorFactory = connectorFactory;
|
||||||
|
this.logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
create(userDN: string, password: string): ISession {
|
||||||
|
const connector = this.connectorFactory.create();
|
||||||
|
return new SafeSession(
|
||||||
|
new Session(
|
||||||
|
userDN,
|
||||||
|
password,
|
||||||
|
this.config,
|
||||||
|
connector,
|
||||||
|
this.logger
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
import Sinon = require("sinon");
|
||||||
|
|
||||||
|
import { ISession } from "./ISession";
|
||||||
|
import { ISessionFactory } from "./ISessionFactory";
|
||||||
|
|
||||||
|
export class SessionFactoryStub implements ISessionFactory {
|
||||||
|
createStub: Sinon.SinonStub;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.createStub = Sinon.stub();
|
||||||
|
}
|
||||||
|
|
||||||
|
create(userDN: string, password: string): ISession {
|
||||||
|
return this.createStub(userDN, password);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
import Bluebird = require("bluebird");
|
||||||
|
import Sinon = require("sinon");
|
||||||
|
|
||||||
|
import { ISession } from "./ISession";
|
||||||
|
|
||||||
|
export class SessionStub implements ISession {
|
||||||
|
openStub: Sinon.SinonStub;
|
||||||
|
closeStub: Sinon.SinonStub;
|
||||||
|
searchUserDnStub: Sinon.SinonStub;
|
||||||
|
searchEmailsStub: Sinon.SinonStub;
|
||||||
|
searchGroupsStub: Sinon.SinonStub;
|
||||||
|
modifyPasswordStub: Sinon.SinonStub;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.openStub = Sinon.stub();
|
||||||
|
this.closeStub = Sinon.stub();
|
||||||
|
this.searchUserDnStub = Sinon.stub();
|
||||||
|
this.searchEmailsStub = Sinon.stub();
|
||||||
|
this.searchGroupsStub = Sinon.stub();
|
||||||
|
this.modifyPasswordStub = Sinon.stub();
|
||||||
|
}
|
||||||
|
|
||||||
|
open(): Bluebird<void> {
|
||||||
|
return this.openStub();
|
||||||
|
}
|
||||||
|
|
||||||
|
close(): Bluebird<void> {
|
||||||
|
return this.closeStub();
|
||||||
|
}
|
||||||
|
|
||||||
|
searchUserDn(username: string): Bluebird<string> {
|
||||||
|
return this.searchUserDnStub(username);
|
||||||
|
}
|
||||||
|
|
||||||
|
searchEmails(username: string): Bluebird<string[]> {
|
||||||
|
return this.searchEmailsStub(username);
|
||||||
|
}
|
||||||
|
|
||||||
|
searchGroups(username: string): Bluebird<string[]> {
|
||||||
|
return this.searchGroupsStub(username);
|
||||||
|
}
|
||||||
|
|
||||||
|
modifyPassword(username: string, newPassword: string): Bluebird<void> {
|
||||||
|
return this.modifyPasswordStub(username, newPassword);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,69 @@
|
||||||
|
import LdapJs = require("ldapjs");
|
||||||
|
import EventEmitter = require("events");
|
||||||
|
import Bluebird = require("bluebird");
|
||||||
|
import { IConnector } from "./IConnector";
|
||||||
|
import Exceptions = require("../../../../Exceptions");
|
||||||
|
|
||||||
|
interface SearchEntry {
|
||||||
|
object: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ClientAsync {
|
||||||
|
on(event: string, callback: (data?: any) => void): void;
|
||||||
|
bindAsync(username: string, password: string): Bluebird<void>;
|
||||||
|
unbindAsync(): Bluebird<void>;
|
||||||
|
searchAsync(base: string, query: LdapJs.SearchOptions): Bluebird<EventEmitter>;
|
||||||
|
modifyAsync(userdn: string, change: LdapJs.Change): Bluebird<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Connector implements IConnector {
|
||||||
|
private client: ClientAsync;
|
||||||
|
|
||||||
|
constructor(url: string, ldapjs: typeof LdapJs) {
|
||||||
|
const ldapClient = ldapjs.createClient({
|
||||||
|
url: url,
|
||||||
|
reconnect: true
|
||||||
|
});
|
||||||
|
|
||||||
|
/*const clientLogger = (ldapClient as any).log;
|
||||||
|
if (clientLogger) {
|
||||||
|
clientLogger.level("trace");
|
||||||
|
}*/
|
||||||
|
|
||||||
|
this.client = Bluebird.promisifyAll(ldapClient) as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
bindAsync(username: string, password: string): Bluebird<void> {
|
||||||
|
return this.client.bindAsync(username, password);
|
||||||
|
}
|
||||||
|
|
||||||
|
unbindAsync(): Bluebird<void> {
|
||||||
|
return this.client.unbindAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
searchAsync(base: string, query: any): Bluebird<any[]> {
|
||||||
|
const that = this;
|
||||||
|
return this.client.searchAsync(base, query)
|
||||||
|
.then(function (res: EventEmitter) {
|
||||||
|
const doc: SearchEntry[] = [];
|
||||||
|
return new Bluebird<any[]>((resolve, reject) => {
|
||||||
|
res.on("searchEntry", function (entry: SearchEntry) {
|
||||||
|
doc.push(entry.object);
|
||||||
|
});
|
||||||
|
res.on("error", function (err: Error) {
|
||||||
|
reject(new Exceptions.LdapSearchError(err.message));
|
||||||
|
});
|
||||||
|
res.on("end", function () {
|
||||||
|
resolve(doc);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(function (err: Error) {
|
||||||
|
return Bluebird.reject(new Exceptions.LdapSearchError(err.message));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
modifyAsync(dn: string, changeRequest: any): Bluebird<void> {
|
||||||
|
return this.client.modifyAsync(dn, changeRequest);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { IConnector } from "./IConnector";
|
||||||
|
import { Connector } from "./Connector";
|
||||||
|
import { LdapConfiguration } from "../../../../configuration/schema/LdapConfiguration";
|
||||||
|
import { Ldapjs } from "Dependencies";
|
||||||
|
|
||||||
|
export class ConnectorFactory {
|
||||||
|
private configuration: LdapConfiguration;
|
||||||
|
private ldapjs: Ldapjs;
|
||||||
|
|
||||||
|
constructor(configuration: LdapConfiguration, ldapjs: Ldapjs) {
|
||||||
|
this.configuration = configuration;
|
||||||
|
this.ldapjs = ldapjs;
|
||||||
|
}
|
||||||
|
|
||||||
|
create(): IConnector {
|
||||||
|
return new Connector(this.configuration.url, this.ldapjs);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
import BluebirdPromise = require("bluebird");
|
||||||
|
import Sinon = require("sinon");
|
||||||
|
|
||||||
|
import { IConnectorFactory } from "./IConnectorFactory";
|
||||||
|
import { IConnector } from "./IConnector";
|
||||||
|
|
||||||
|
export class ConnectorFactoryStub implements IConnectorFactory {
|
||||||
|
createStub: Sinon.SinonStub;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.createStub = Sinon.stub();
|
||||||
|
}
|
||||||
|
|
||||||
|
create(): IConnector {
|
||||||
|
return this.createStub();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
import BluebirdPromise = require("bluebird");
|
||||||
|
import Sinon = require("sinon");
|
||||||
|
|
||||||
|
import { IConnector } from "./IConnector";
|
||||||
|
|
||||||
|
export class ConnectorStub implements IConnector {
|
||||||
|
bindAsyncStub: Sinon.SinonStub;
|
||||||
|
unbindAsyncStub: Sinon.SinonStub;
|
||||||
|
searchAsyncStub: Sinon.SinonStub;
|
||||||
|
modifyAsyncStub: Sinon.SinonStub;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.bindAsyncStub = Sinon.stub();
|
||||||
|
this.unbindAsyncStub = Sinon.stub();
|
||||||
|
this.searchAsyncStub = Sinon.stub();
|
||||||
|
this.modifyAsyncStub = Sinon.stub();
|
||||||
|
}
|
||||||
|
|
||||||
|
bindAsync(username: string, password: string): BluebirdPromise<void> {
|
||||||
|
return this.bindAsyncStub(username, password);
|
||||||
|
}
|
||||||
|
|
||||||
|
unbindAsync(): BluebirdPromise<void> {
|
||||||
|
return this.unbindAsyncStub();
|
||||||
|
}
|
||||||
|
|
||||||
|
searchAsync(base: string, query: any): BluebirdPromise<any[]> {
|
||||||
|
return this.searchAsyncStub(base, query);
|
||||||
|
}
|
||||||
|
|
||||||
|
modifyAsync(dn: string, changeRequest: any): BluebirdPromise<void> {
|
||||||
|
return this.modifyAsyncStub(dn, changeRequest);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
import Bluebird = require("bluebird");
|
||||||
|
import EventEmitter = require("events");
|
||||||
|
|
||||||
|
export interface IConnector {
|
||||||
|
bindAsync(username: string, password: string): Bluebird<void>;
|
||||||
|
unbindAsync(): Bluebird<void>;
|
||||||
|
searchAsync(base: string, query: any): Bluebird<any[]>;
|
||||||
|
modifyAsync(dn: string, changeRequest: any): Bluebird<void>;
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { IConnector } from "./IConnector";
|
||||||
|
|
||||||
|
export interface IConnectorFactory {
|
||||||
|
create(): IConnector;
|
||||||
|
}
|